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
[Student Libraries] Exporter UI #31303
Changes from all commits
2e12bb7
4667d00
f869c66
68f1e98
2cb88dd
03d06bb
ca2836f
c6be322
6d215d7
5f8d3bc
8077f2b
7347b3f
34b2c15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,34 @@ | ||
/*global dashboard*/ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import BaseDialog from '../../../templates/BaseDialog'; | ||
import Dialog, {Body} from '@cdo/apps/templates/Dialog'; | ||
import {connect} from 'react-redux'; | ||
import {hideLibraryCreationDialog} from '../shareDialogRedux'; | ||
import libraryParser from './libraryParser'; | ||
import LibraryClientApi from './LibraryClientApi'; | ||
import i18n from '@cdo/locale'; | ||
import PadAndCenter from '@cdo/apps/templates/teacherDashboard/PadAndCenter'; | ||
import {Heading1, Heading2} from '@cdo/apps/lib/ui/Headings'; | ||
|
||
const styles = { | ||
alert: { | ||
color: 'red' | ||
}, | ||
libraryBoundary: { | ||
padding: 10 | ||
}, | ||
largerCheckbox: { | ||
width: 20, | ||
height: 20, | ||
margin: 10 | ||
}, | ||
functionItem: { | ||
marginBottom: 20 | ||
}, | ||
textarea: { | ||
width: 400 | ||
} | ||
}; | ||
|
||
class LibraryCreationDialog extends React.Component { | ||
static propTypes = { | ||
|
@@ -18,9 +40,10 @@ class LibraryCreationDialog extends React.Component { | |
state = { | ||
clientApi: new LibraryClientApi(this.props.channelId), | ||
librarySource: '', | ||
selectedFunctionList: [], | ||
sourceFunctionList: [], | ||
loadingFinished: false, | ||
libraryName: '' | ||
libraryName: '', | ||
canPublish: false | ||
}; | ||
|
||
componentDidUpdate(prevProps) { | ||
|
@@ -37,7 +60,7 @@ class LibraryCreationDialog extends React.Component { | |
), | ||
librarySource: response.source, | ||
loadingFinished: true, | ||
selectedFunctionList: libraryParser.getFunctions(response.source) | ||
sourceFunctionList: libraryParser.getFunctions(response.source) | ||
}); | ||
}); | ||
}; | ||
|
@@ -48,11 +71,24 @@ class LibraryCreationDialog extends React.Component { | |
}; | ||
|
||
publish = () => { | ||
let formElements = document.getElementById('selectFunction').elements; | ||
let selectedFunctionList = []; | ||
let libraryDescription = ''; | ||
[...formElements].forEach(element => { | ||
if (element.type === 'checkbox' && element.checked) { | ||
selectedFunctionList.push(this.state.sourceFunctionList[element.value]); | ||
} | ||
if (element.type === 'textarea') { | ||
libraryDescription = element.value; | ||
} | ||
}); | ||
let libraryJson = libraryParser.createLibraryJson( | ||
this.state.librarySource, | ||
this.state.selectedFunctionList, | ||
this.state.libraryName | ||
selectedFunctionList, | ||
this.state.libraryName, | ||
libraryDescription | ||
); | ||
|
||
// TODO: Display final version of error and success messages to the user. | ||
this.state.clientApi.publish( | ||
libraryJson, | ||
|
@@ -67,37 +103,97 @@ class LibraryCreationDialog extends React.Component { | |
); | ||
}; | ||
|
||
validateInput = () => { | ||
// Check if any of the checkboxes are checked | ||
// If this changes the publishable state, update | ||
let formElements = document.getElementById('selectFunction').elements; | ||
let isChecked = false; | ||
[...formElements].forEach(element => { | ||
if (element.type === 'checkbox' && element.checked) { | ||
isChecked = true; | ||
} | ||
}); | ||
if (isChecked !== this.state.canPublish) { | ||
this.setState({canPublish: isChecked}); | ||
} | ||
}; | ||
|
||
displayFunctions = () => { | ||
if (!this.state.loadingFinished) { | ||
return <div>Loading...</div>; | ||
return <div id="loading">Loading...</div>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you get the chance, it would be nice to make this a spinner instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
let keyIndex = 0; | ||
return ( | ||
<div> | ||
<div>{this.state.libraryName}</div> | ||
{this.state.selectedFunctionList.map(selectedFunction => { | ||
let name = selectedFunction.functionName; | ||
return <div key={name}>{name}</div>; | ||
})} | ||
<Heading2> | ||
<b>{i18n.libraryName()}</b> | ||
{this.state.libraryName} | ||
</Heading2> | ||
<form id="selectFunction" onSubmit={this.publish}> | ||
<textarea | ||
required | ||
name="description" | ||
rows="2" | ||
cols="200" | ||
style={styles.textarea} | ||
placeholder="Write a description of your library" | ||
/> | ||
{this.state.sourceFunctionList.map(sourceFunction => { | ||
let name = sourceFunction.functionName; | ||
let comment = sourceFunction.comment; | ||
return ( | ||
<div key={keyIndex} style={styles.functionItem}> | ||
<input | ||
type="checkbox" | ||
style={styles.largerCheckbox} | ||
disabled={comment.length === 0} | ||
onClick={this.validateInput} | ||
value={keyIndex++} | ||
/> | ||
{name} | ||
<br /> | ||
{comment.length === 0 && ( | ||
<p style={styles.alert}> | ||
{i18n.libraryExportNoCommentError()} | ||
</p> | ||
)} | ||
<pre>{comment}</pre> | ||
</div> | ||
); | ||
})} | ||
<input | ||
className="btn btn-primary" | ||
type="submit" | ||
value={i18n.publish()} | ||
disabled={!this.state.canPublish} | ||
/> | ||
</form> | ||
</div> | ||
); | ||
}; | ||
|
||
render() { | ||
return ( | ||
<BaseDialog | ||
<Dialog | ||
isOpen={this.props.dialogIsOpen} | ||
handleClose={this.handleClose} | ||
useUpdatedStyles | ||
> | ||
{this.displayFunctions()} | ||
<button type="button" onClick={this.publish}> | ||
{i18n.publish()} | ||
</button> | ||
</BaseDialog> | ||
<Body> | ||
<PadAndCenter> | ||
<div style={styles.libraryBoundary}> | ||
<Heading1>{i18n.libraryExportTitle()}</Heading1> | ||
{this.displayFunctions()} | ||
</div> | ||
</PadAndCenter> | ||
</Body> | ||
</Dialog> | ||
); | ||
} | ||
} | ||
|
||
export const UnconnectedLibraryCreationDialog = LibraryCreationDialog; | ||
|
||
export default connect( | ||
state => ({ | ||
dialogIsOpen: state.shareDialog.libraryDialogIsOpen | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import {expect} from '../../../../util/configuredChai'; | ||
import React from 'react'; | ||
import {mount} from 'enzyme'; | ||
import {UnconnectedLibraryCreationDialog as LibraryCreationDialog} from '@cdo/apps/code-studio/components/Libraries/LibraryCreationDialog.jsx'; | ||
import libraryParser from '@cdo/apps/code-studio/components/Libraries/libraryParser'; | ||
|
||
const LIBRARY_SOURCE = | ||
'/*\n' + | ||
'Everything in this block comment\n' + | ||
'will be included as-is\n' + | ||
'\n' + | ||
'including the whitespace above it\n' + | ||
'*/\n' + | ||
'function myFunc2(n) {\n' + | ||
'}\n' + | ||
'\n' + | ||
'// This comment will not be included\n' + | ||
'/* \n' + | ||
'This comment will be included.\n' + | ||
'*/\n' + | ||
'function myFunc3() {\n' + | ||
'}\n' + | ||
'\n' + | ||
'/**/\n' + | ||
'function theAboveCommentWillNotBreakThings() {\n' + | ||
'}'; | ||
|
||
describe('LibraryCreationDialog', () => { | ||
let wrapper; | ||
beforeEach(() => { | ||
wrapper = mount( | ||
<LibraryCreationDialog | ||
channelId={'123'} | ||
dialogIsOpen={true} | ||
onClose={() => {}} | ||
/> | ||
); | ||
}); | ||
|
||
const SUBMIT_SELECTOR = 'input[type="submit"]'; | ||
const CHECKBOX_SELECTOR = 'input[type="checkbox"]'; | ||
|
||
it('publish is disabled when nothing checked', () => { | ||
wrapper.setState({ | ||
libraryName: 'testLibrary', | ||
librarySource: LIBRARY_SOURCE, | ||
loadingFinished: true, | ||
sourceFunctionList: libraryParser.getFunctions(LIBRARY_SOURCE) | ||
}); | ||
wrapper.update(); | ||
|
||
expect(wrapper.find(SUBMIT_SELECTOR)).to.be.disabled(); | ||
}); | ||
|
||
it('publish is enabled when something is checked', () => { | ||
wrapper.setState({ | ||
libraryName: 'testLibrary', | ||
librarySource: LIBRARY_SOURCE, | ||
loadingFinished: true, | ||
sourceFunctionList: libraryParser.getFunctions(LIBRARY_SOURCE), | ||
canPublish: true | ||
}); | ||
wrapper.update(); | ||
|
||
expect(wrapper.find(SUBMIT_SELECTOR)).not.to.be.disabled(); | ||
}); | ||
|
||
it('checkbox is diabled for items without comments', () => { | ||
wrapper.setState({ | ||
libraryName: 'testLibrary', | ||
librarySource: LIBRARY_SOURCE, | ||
loadingFinished: true, | ||
sourceFunctionList: libraryParser.getFunctions(LIBRARY_SOURCE) | ||
}); | ||
|
||
wrapper.update(); | ||
|
||
expect(wrapper.find(CHECKBOX_SELECTOR).last()).to.be.disabled(); | ||
}); | ||
|
||
it('displays loading while in the loading state', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome |
||
wrapper.setState({ | ||
loadingFinished: false | ||
}); | ||
wrapper.update(); | ||
expect(wrapper.find(SUBMIT_SELECTOR)).not.to.exist; | ||
expect(wrapper.find('#loading')).to.exist; | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: (I don't feel too strongly about this) This seems like it would be more maintainable as a check by class name rather than type. i.e. it's not too unlikely that we'd add another textarea somewhere.