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 success page #31487

Merged
merged 12 commits into from
Oct 29, 2019
3 changes: 3 additions & 0 deletions apps/i18n/common/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@
"contractMatchBadRange": "Your contract has the wrong range.",
"controlProjectSharing": "Control project sharing",
"copy": "Copy",
"copyId": "Copy ID",
"copyStudentsConfirm": "Yes, I want to copy student(s) to be in this current section AND the new section.",
"copyright": "Copyright",
"correct": "Correct",
Expand Down Expand Up @@ -794,6 +795,8 @@
"libraryExportNoCommentError": "This function cannot be exported until you add a comment to it.",
"libraryExportTitle": "Export Functions as a Library",
"libraryName": "Library Name:",
"libraryPublishTitle": "Successfully published your library: ",
"libraryPublishExplanation": "Share this ID with others so they can use your library in their project:",
"linesOfCode": "Lines of Code",
"listVariable": "list",
"loading": "Loading...",
Expand Down
120 changes: 98 additions & 22 deletions apps/src/code-studio/components/libraries/LibraryCreationDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import i18n from '@cdo/locale';
import PadAndCenter from '@cdo/apps/templates/teacherDashboard/PadAndCenter';
import {Heading1, Heading2} from '@cdo/apps/lib/ui/Headings';
import Spinner from '../../pd/components/spinner';
import Button from '@cdo/apps/templates/Button';

const styles = {
alert: {
Expand All @@ -28,12 +29,35 @@ const styles = {
textarea: {
width: 400
},
centerSpinner: {
centerContent: {
display: 'flex',
justifyContent: 'center'
},
copy: {
cursor: 'copy',
width: 300,
height: 25
},
button: {
marginLeft: 10,
marginRight: 10
}
};

function select(event) {
event.target.select();
}

/**
* @readonly
* @enum {string}
*/
export const LoadingState = {
LOADING: 'loading',
DONE_LOADING: 'done_loading',
PUBLISHED: 'published'
};

class LibraryCreationDialog extends React.Component {
static propTypes = {
dialogIsOpen: PropTypes.bool.isRequired,
Expand All @@ -44,7 +68,7 @@ class LibraryCreationDialog extends React.Component {
state = {
librarySource: '',
sourceFunctionList: [],
loadingFinished: false,
loadingState: LoadingState.LOADING,
libraryName: '',
canPublish: false
};
Expand All @@ -62,17 +86,23 @@ class LibraryCreationDialog extends React.Component {
dashboard.project.getLevelName()
),
librarySource: response.source,
loadingFinished: true,
loadingState: LoadingState.DONE_LOADING,
sourceFunctionList: libraryParser.getFunctions(response.source)
});
});
};

handleClose = () => {
this.setState({loadingFinished: false});
this.setState({loadingState: LoadingState.LOADING});
this.props.onClose();
};

copyChannelId = () => {
let channelId = document.getElementById('library-sharing');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use state rather than searching the dom for the element?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed a like a good enough solution. clipboard isn't as supported in browsers as I would like for something like this and I wasn't sure if it was worth bringing in an additional library. Happy to discuss further.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth a quick chat - I'm curious about the clipboard :) Shouldn't block this PR though.

channelId.select();
document.execCommand('copy');
};

publish = event => {
event.preventDefault();
let formElements = this.formElements.elements;
Expand All @@ -99,10 +129,8 @@ class LibraryCreationDialog extends React.Component {
error => {
console.warn(`Error publishing library: ${error}`);
},
data => {
console.log(
`Successfully published library. VersionID: ${data.versionId}`
);
() => {
this.setState({loadingState: LoadingState.PUBLISHED});
}
);
dashboard.project.setLibraryName(this.state.libraryName);
Expand All @@ -124,14 +152,15 @@ class LibraryCreationDialog extends React.Component {
}
};

displayLoadingState = () => {
return (
<div style={styles.centerContent}>
<Spinner />
</div>
);
};

displayFunctions = () => {
if (!this.state.loadingFinished) {
return (
<div style={styles.centerSpinner}>
<Spinner />
</div>
);
}
let keyIndex = 0;
return (
<div>
Expand Down Expand Up @@ -176,18 +205,65 @@ class LibraryCreationDialog extends React.Component {
</div>
);
})}
<input
className="btn btn-primary"
type="submit"
value={i18n.publish()}
disabled={!this.state.canPublish}
/>
<div>
<input
className="btn btn-primary"
type="submit"
value={i18n.publish()}
disabled={!this.state.canPublish}
/>
{this.state.loadingState === LoadingState.ERROR_PUBLISH && (
<p id="error-alert" style={styles.alert}>
{i18n.libraryPublishFail()}
</p>
)}
</div>
</form>
</div>
);
};

displaySuccess = () => {
return (
<div>
<Heading2>
<b>{i18n.libraryPublishTitle()}</b>
{this.state.libraryName}
</Heading2>
<div>
<p>{i18n.libraryPublishExplanation()}</p>
<div style={styles.centerContent}>
<input
type="text"
id="library-sharing"
onClick={select}
readOnly="true"
value={dashboard.project.getCurrentId()}
style={styles.copy}
/>
<Button
onClick={this.copyChannelId}
text={i18n.copyId()}
style={styles.button}
/>
</div>
</div>
</div>
);
};

render() {
let bodyContent;
switch (this.state.loadingState) {
case LoadingState.LOADING:
bodyContent = this.displayLoadingState();
break;
case LoadingState.PUBLISHED:
bodyContent = this.displaySuccess();
break;
default:
bodyContent = this.displayFunctions();
}
return (
<Dialog
isOpen={this.props.dialogIsOpen}
Expand All @@ -198,7 +274,7 @@ class LibraryCreationDialog extends React.Component {
<PadAndCenter>
<div style={styles.libraryBoundary}>
<Heading1>{i18n.libraryExportTitle()}</Heading1>
{this.displayFunctions()}
{bodyContent}
</div>
</PadAndCenter>
</Body>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {expect, assert} from '../../../../util/reconfiguredChai';
import React from 'react';
import {mount} from 'enzyme';
import {UnconnectedLibraryCreationDialog as LibraryCreationDialog} from '@cdo/apps/code-studio/components/libraries/LibraryCreationDialog.jsx';
import {
UnconnectedLibraryCreationDialog as LibraryCreationDialog,
LoadingState
} from '@cdo/apps/code-studio/components/libraries/LibraryCreationDialog.jsx';
import libraryParser from '@cdo/apps/code-studio/components/libraries/libraryParser';
import LibraryClientApi from '@cdo/apps/code-studio/components/libraries/LibraryClientApi';
import sinon from 'sinon';
import Spinner from '@cdo/apps/code-studio/pd/components/spinner';
import {replaceOnWindow, restoreOnWindow} from '../../../../util/testUtils';

const LIBRARY_SOURCE =
'/*\n' +
Expand All @@ -30,12 +34,34 @@ const LIBRARY_SOURCE =

describe('LibraryCreationDialog', () => {
let wrapper;
let clientApi = new LibraryClientApi('123');
let publishSpy = sinon.stub(clientApi, 'publish');
let clientApi;
let publishSpy;

const SUBMIT_SELECTOR = 'input[type="submit"]';
const CHECKBOX_SELECTOR = 'input[type="checkbox"]';
const DESCRIPTION_SELECTOR = 'textarea';
const CHANNEL_ID_SELECTOR = 'input[type="text"]';

before(() => {
replaceOnWindow('dashboard', {
project: {
setLibraryName: () => {},
setLibraryDescription: () => {},
getCurrentId: () => {}
}
});
sinon.stub(window.dashboard.project, 'setLibraryName').returns(undefined);
sinon
.stub(window.dashboard.project, 'setLibraryDescription')
.returns(undefined);
sinon.stub(window.dashboard.project, 'getCurrentId').returns('123');
clientApi = new LibraryClientApi('123');
publishSpy = sinon.stub(clientApi, 'publish');
});

after(() => {
restoreOnWindow('dashboard');
});

beforeEach(() => {
wrapper = mount(
Expand All @@ -56,7 +82,7 @@ describe('LibraryCreationDialog', () => {
wrapper.setState({
libraryName: 'testLibrary',
librarySource: LIBRARY_SOURCE,
loadingFinished: true,
loadingState: LoadingState.DONE_LOADING,
sourceFunctionList: libraryParser.getFunctions(LIBRARY_SOURCE)
});

Expand All @@ -67,7 +93,7 @@ describe('LibraryCreationDialog', () => {
wrapper.setState({
libraryName: 'testLibrary',
librarySource: LIBRARY_SOURCE,
loadingFinished: true,
loadingState: LoadingState.DONE_LOADING,
sourceFunctionList: libraryParser.getFunctions(LIBRARY_SOURCE)
});

Expand All @@ -86,7 +112,7 @@ describe('LibraryCreationDialog', () => {
wrapper.setState({
libraryName: 'testLibrary',
librarySource: LIBRARY_SOURCE,
loadingFinished: true,
loadingState: LoadingState.DONE_LOADING,
sourceFunctionList: libraryParser.getFunctions(LIBRARY_SOURCE)
});

Expand All @@ -107,7 +133,7 @@ describe('LibraryCreationDialog', () => {
wrapper.setState({
libraryName: 'testLibrary',
librarySource: LIBRARY_SOURCE,
loadingFinished: true,
loadingState: LoadingState.DONE_LOADING,
sourceFunctionList: libraryParser.getFunctions(LIBRARY_SOURCE)
});

Expand All @@ -118,6 +144,19 @@ describe('LibraryCreationDialog', () => {
.prop('required')
);
});

it('displays channel id when in published state', () => {
wrapper.setState({
libraryName: 'testLibrary',
librarySource: LIBRARY_SOURCE,
loadingState: LoadingState.PUBLISHED,
sourceFunctionList: libraryParser.getFunctions(LIBRARY_SOURCE)
});

assert.isTrue(
wrapper.find(CHANNEL_ID_SELECTOR).instance().value === '123'
);
});
});

describe('publish', () => {
Expand All @@ -126,7 +165,7 @@ describe('LibraryCreationDialog', () => {
wrapper.setState({
libraryName: 'testLibrary',
librarySource: LIBRARY_SOURCE,
loadingFinished: true,
loadingState: LoadingState.DONE_LOADING,
sourceFunctionList: functionList
});

Expand Down