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

[Student Libraries] Exporter UI #31303

Merged
merged 13 commits into from Oct 21, 2019
3 changes: 3 additions & 0 deletions apps/i18n/common/en_us.json
Expand Up @@ -786,6 +786,9 @@
"levelsAttempted": "Levels attempted in",
"levelStatus": "Level Status",
"levelType": "Level Type",
"libraryExportNoCommentError": "This function cannot be exported until you add a comment to it.",
"libraryExportTitle": "Export Functions as a Library",
"libraryName": "Library Name:",
"linesOfCode": "Lines of Code",
"listVariable": "list",
"loading": "Loading...",
Expand Down
132 changes: 114 additions & 18 deletions apps/src/code-studio/components/Libraries/LibraryCreationDialog.jsx
@@ -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 = {
Expand All @@ -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) {
Expand All @@ -37,7 +60,7 @@ class LibraryCreationDialog extends React.Component {
),
librarySource: response.source,
loadingFinished: true,
selectedFunctionList: libraryParser.getFunctions(response.source)
sourceFunctionList: libraryParser.getFunctions(response.source)
});
});
};
Expand All @@ -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') {
Copy link
Contributor

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.

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,
Expand All @@ -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>;
Copy link
Contributor

Choose a reason for hiding this comment

The 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 Loading...

Copy link
Author

Choose a reason for hiding this comment

The 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
Expand Down
12 changes: 10 additions & 2 deletions apps/src/code-studio/components/Libraries/libraryParser.js
Expand Up @@ -82,14 +82,21 @@ function createLibraryClosure(code, functions, libraryName) {
* @param {string} code The code that makes up the library
* @param {array} selectedFunctions The list of functions that will be exported from the library
* @param {string} libraryName The name of the library to be exported
* @param {string} libraryDescription The description of the library to be exported
* @returns {null,string} null if there is an error. Else, a JSON string representing the library
*/
export function createLibraryJson(code, selectedFunctions, libraryName) {
export function createLibraryJson(
code,
selectedFunctions,
libraryName,
libraryDescription
) {
if (
typeof code !== 'string' ||
!Array.isArray(selectedFunctions) ||
typeof libraryName !== 'string' ||
!libraryName
!libraryName ||
typeof libraryDescription !== 'string'
) {
return;
}
Expand All @@ -102,6 +109,7 @@ export function createLibraryJson(code, selectedFunctions, libraryName) {

return JSON.stringify({
name: libraryName,
description: libraryDescription,
dropletConfig: config,
source: closure
});
Expand Down
@@ -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', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
});
});