Feature(challenges): add bug modal and logic
This commit is contained in:
80
common/app/routes/challenges/components/Bug-Modal.jsx
Normal file
80
common/app/routes/challenges/components/Bug-Modal.jsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Button, Modal } from 'react-bootstrap';
|
||||||
|
import PureComponent from 'react-pure-render/component';
|
||||||
|
import { createIssue, openIssueSearch, closeBugModal } from '../redux/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({ isOpen: state.challengesApp.isBugOpen });
|
||||||
|
const actions = { createIssue, openIssueSearch, closeBugModal };
|
||||||
|
const bugLink = 'https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/' +
|
||||||
|
'FreeCodeCamp-Report-Bugs';
|
||||||
|
|
||||||
|
export class BugModal extends PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
closeBugModal: PropTypes.func,
|
||||||
|
openIssueSearch: PropTypes.func,
|
||||||
|
createIssue: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
closeBugModal,
|
||||||
|
openIssueSearch,
|
||||||
|
createIssue
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onHide={ closeBugModal }
|
||||||
|
show={ isOpen }
|
||||||
|
>
|
||||||
|
<Modal.Header className='challenge-list-header'>
|
||||||
|
Did you find a bug?
|
||||||
|
<span className='close closing-x'>×</span>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className='text-center'>
|
||||||
|
<h3>
|
||||||
|
Before you submit a new issue,
|
||||||
|
read "Help I've Found a Bug" and
|
||||||
|
browse other issues with this challenge.
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
href={ bugLink }
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
Read "Help I've Found a Bug"
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
onClick={ openIssueSearch }
|
||||||
|
>
|
||||||
|
Browse other issues with this challenge
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
onClick={ createIssue }
|
||||||
|
>
|
||||||
|
Create my GitHub issue
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, actions)(BugModal);
|
@ -7,6 +7,7 @@ import PureComponent from 'react-pure-render/component';
|
|||||||
import Editor from './Editor.jsx';
|
import Editor from './Editor.jsx';
|
||||||
import SidePanel from './Side-Panel.jsx';
|
import SidePanel from './Side-Panel.jsx';
|
||||||
import Preview from './Preview.jsx';
|
import Preview from './Preview.jsx';
|
||||||
|
import BugModal from '../Bug-Modal.jsx';
|
||||||
import { challengeSelector } from '../../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
import {
|
import {
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
@ -102,6 +103,7 @@ export class Challenge extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
{ this.renderPreview(showPreview) }
|
{ this.renderPreview(showPreview) }
|
||||||
|
<BugModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,11 @@ import TestSuite from './Test-Suite.jsx';
|
|||||||
import Output from './Output.jsx';
|
import Output from './Output.jsx';
|
||||||
import ToolPanel from './Tool-Panel.jsx';
|
import ToolPanel from './Tool-Panel.jsx';
|
||||||
import { challengeSelector } from '../../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
import { updateHint, executeChallenge } from '../../redux/actions';
|
import {
|
||||||
|
openBugModal,
|
||||||
|
updateHint,
|
||||||
|
executeChallenge
|
||||||
|
} from '../../redux/actions';
|
||||||
import { makeToast } from '../../../../toasts/redux/actions';
|
import { makeToast } from '../../../../toasts/redux/actions';
|
||||||
import { toggleHelpChat } from '../../../../redux/actions';
|
import { toggleHelpChat } from '../../../../redux/actions';
|
||||||
|
|
||||||
@ -17,7 +21,8 @@ const bindableActions = {
|
|||||||
makeToast,
|
makeToast,
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
updateHint,
|
updateHint,
|
||||||
toggleHelpChat
|
toggleHelpChat,
|
||||||
|
openBugModal
|
||||||
};
|
};
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
@ -59,7 +64,8 @@ export class SidePanel extends PureComponent {
|
|||||||
hints: PropTypes.string,
|
hints: PropTypes.string,
|
||||||
updateHint: PropTypes.func,
|
updateHint: PropTypes.func,
|
||||||
makeToast: PropTypes.func,
|
makeToast: PropTypes.func,
|
||||||
toggleHelpChat: PropTypes.func
|
toggleHelpChat: PropTypes.func,
|
||||||
|
openBugModal: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
||||||
@ -99,7 +105,8 @@ export class SidePanel extends PureComponent {
|
|||||||
executeChallenge,
|
executeChallenge,
|
||||||
updateHint,
|
updateHint,
|
||||||
makeToast,
|
makeToast,
|
||||||
toggleHelpChat
|
toggleHelpChat,
|
||||||
|
openBugModal
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const style = {
|
const style = {
|
||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
@ -131,6 +138,7 @@ export class SidePanel extends PureComponent {
|
|||||||
executeChallenge={ executeChallenge }
|
executeChallenge={ executeChallenge }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
makeToast={ makeToast }
|
makeToast={ makeToast }
|
||||||
|
openBugModal={ openBugModal }
|
||||||
toggleHelpChat={ toggleHelpChat }
|
toggleHelpChat={ toggleHelpChat }
|
||||||
updateHint={ updateHint }
|
updateHint={ updateHint }
|
||||||
/>
|
/>
|
||||||
|
@ -14,7 +14,8 @@ export default class ToolPanel extends PureComponent {
|
|||||||
executeChallenge: PropTypes.func,
|
executeChallenge: PropTypes.func,
|
||||||
updateHint: PropTypes.func,
|
updateHint: PropTypes.func,
|
||||||
hint: PropTypes.string,
|
hint: PropTypes.string,
|
||||||
toggleHelpChat: PropTypes.func
|
toggleHelpChat: PropTypes.func,
|
||||||
|
openBugModal: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
makeHint() {
|
makeHint() {
|
||||||
@ -54,7 +55,8 @@ export default class ToolPanel extends PureComponent {
|
|||||||
const {
|
const {
|
||||||
hint,
|
hint,
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
toggleHelpChat
|
toggleHelpChat,
|
||||||
|
openBugModal
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -92,6 +94,7 @@ export default class ToolPanel extends PureComponent {
|
|||||||
bsSize='large'
|
bsSize='large'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
componentClass='label'
|
componentClass='label'
|
||||||
|
onClick={ openBugModal }
|
||||||
>
|
>
|
||||||
Bug
|
Bug
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -7,6 +7,7 @@ import PureComponent from 'react-pure-render/component';
|
|||||||
import { Col } from 'react-bootstrap';
|
import { Col } from 'react-bootstrap';
|
||||||
import SidePanel from './Side-Panel.jsx';
|
import SidePanel from './Side-Panel.jsx';
|
||||||
import ToolPanel from './Tool-Panel.jsx';
|
import ToolPanel from './Tool-Panel.jsx';
|
||||||
|
import BugModal from '../Bug-Modal.jsx';
|
||||||
|
|
||||||
import { challengeSelector } from '../../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
|
|
||||||
@ -71,6 +72,7 @@ export class Project extends PureComponent {
|
|||||||
<br />
|
<br />
|
||||||
<ToolPanel />
|
<ToolPanel />
|
||||||
<br />
|
<br />
|
||||||
|
<BugModal />
|
||||||
</Col>
|
</Col>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
BackEndForm
|
BackEndForm
|
||||||
} from './Forms.jsx';
|
} from './Forms.jsx';
|
||||||
|
|
||||||
import { submitChallenge } from '../../redux/actions';
|
import { submitChallenge, openBugModal } from '../../redux/actions';
|
||||||
import { challengeSelector } from '../../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
import {
|
import {
|
||||||
simpleProject,
|
simpleProject,
|
||||||
@ -19,7 +19,8 @@ import { toggleHelpChat } from '../../../../redux/actions';
|
|||||||
|
|
||||||
const bindableActions = {
|
const bindableActions = {
|
||||||
submitChallenge,
|
submitChallenge,
|
||||||
toggleHelpChat
|
toggleHelpChat,
|
||||||
|
openBugModal
|
||||||
};
|
};
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
@ -43,7 +44,8 @@ export class ToolPanel extends PureComponent {
|
|||||||
isSimple: PropTypes.bool,
|
isSimple: PropTypes.bool,
|
||||||
isFrontEnd: PropTypes.bool,
|
isFrontEnd: PropTypes.bool,
|
||||||
isSubmitting: PropTypes.bool,
|
isSubmitting: PropTypes.bool,
|
||||||
toggleHelpChat: PropTypes.func
|
toggleHelpChat: PropTypes.func,
|
||||||
|
openBugModal: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
renderSubmitButton(isSignedIn, submitChallenge) {
|
renderSubmitButton(isSignedIn, submitChallenge) {
|
||||||
@ -69,7 +71,8 @@ export class ToolPanel extends PureComponent {
|
|||||||
isSignedIn,
|
isSignedIn,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
submitChallenge,
|
submitChallenge,
|
||||||
toggleHelpChat
|
toggleHelpChat,
|
||||||
|
openBugModal
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
|
const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
|
||||||
@ -94,6 +97,7 @@ export class ToolPanel extends PureComponent {
|
|||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='btn-primary-ghost btn-big'
|
className='btn-primary-ghost btn-big'
|
||||||
componentClass='div'
|
componentClass='div'
|
||||||
|
onClick={ openBugModal }
|
||||||
>
|
>
|
||||||
Bug
|
Bug
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -139,3 +139,9 @@ export const endShake = createAction(types.primeNextQuestion);
|
|||||||
|
|
||||||
export const goToNextQuestion = createAction(types.goToNextQuestion);
|
export const goToNextQuestion = createAction(types.goToNextQuestion);
|
||||||
export const videoCompleted = createAction(types.videoCompleted);
|
export const videoCompleted = createAction(types.videoCompleted);
|
||||||
|
|
||||||
|
// bug
|
||||||
|
export const openBugModal = createAction(types.openBugModal);
|
||||||
|
export const closeBugModal = createAction(types.closeBugModal);
|
||||||
|
export const openIssueSearch = createAction(types.openIssueSearch);
|
||||||
|
export const createIssue = createAction(types.createIssue);
|
||||||
|
72
common/app/routes/challenges/redux/bug-saga.js
Normal file
72
common/app/routes/challenges/redux/bug-saga.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import dedent from 'dedent';
|
||||||
|
|
||||||
|
import types from '../redux/types';
|
||||||
|
import { closeBugModal } from '../redux/actions';
|
||||||
|
|
||||||
|
function filesToMarkdown(files = {}) {
|
||||||
|
const moreThenOneFile = Object.keys(files).length > 1;
|
||||||
|
return Object.keys(files).reduce((fileString, key) => {
|
||||||
|
const file = files[key];
|
||||||
|
if (!file) {
|
||||||
|
return fileString;
|
||||||
|
}
|
||||||
|
const fileName = moreThenOneFile ? `\\ file: ${file.contents}` : '';
|
||||||
|
const fileType = file.ext;
|
||||||
|
return fileString + dedent`
|
||||||
|
\`\`\`${fileType}
|
||||||
|
${fileName}
|
||||||
|
${file.contents}
|
||||||
|
\`\`\`
|
||||||
|
\n
|
||||||
|
`;
|
||||||
|
}, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function bugSaga(actions$, getState, { window }) {
|
||||||
|
return actions$
|
||||||
|
.filter(({ type }) => (
|
||||||
|
type === types.openIssueSearch ||
|
||||||
|
type === types.createIssue
|
||||||
|
))
|
||||||
|
.map(({ type }) => {
|
||||||
|
const {
|
||||||
|
challengesApp: {
|
||||||
|
challenge: challengeName,
|
||||||
|
files
|
||||||
|
}
|
||||||
|
} = getState();
|
||||||
|
const {
|
||||||
|
navigator: { userAgent },
|
||||||
|
location: { href }
|
||||||
|
} = window;
|
||||||
|
if (type === types.openIssueSearch) {
|
||||||
|
window.open(
|
||||||
|
'https://github.com/FreeCodeCamp/FreeCodeCamp/issues?q=' +
|
||||||
|
'is:issue is:all ' +
|
||||||
|
challengeName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let textMessage = [
|
||||||
|
'Challenge [',
|
||||||
|
challengeName,
|
||||||
|
'](',
|
||||||
|
href,
|
||||||
|
') has an issue.\n',
|
||||||
|
'User Agent is: <code>',
|
||||||
|
userAgent,
|
||||||
|
'</code>.\n',
|
||||||
|
'Please describe how to reproduce this issue, and include ',
|
||||||
|
'links to screenshots if possible.\n\n'
|
||||||
|
].join('');
|
||||||
|
const body = filesToMarkdown(files);
|
||||||
|
if (body.length > 10) {
|
||||||
|
textMessage = textMessage + body;
|
||||||
|
}
|
||||||
|
window.open(
|
||||||
|
'https://github.com/freecodecamp/freecodecamp/issues/new?&body=' +
|
||||||
|
window.encodeURIComponent(textMessage),
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
return closeBugModal();
|
||||||
|
});
|
||||||
|
}
|
@ -3,6 +3,7 @@ import completionSaga from './completion-saga';
|
|||||||
import nextChallengeSaga from './next-challenge-saga';
|
import nextChallengeSaga from './next-challenge-saga';
|
||||||
import answerSaga from './answer-saga';
|
import answerSaga from './answer-saga';
|
||||||
import resetChallengeSaga from './reset-challenge-saga';
|
import resetChallengeSaga from './reset-challenge-saga';
|
||||||
|
import bugSaga from './bug-saga';
|
||||||
|
|
||||||
export * as actions from './actions';
|
export * as actions from './actions';
|
||||||
export reducer from './reducer';
|
export reducer from './reducer';
|
||||||
@ -15,5 +16,6 @@ export const sagas = [
|
|||||||
completionSaga,
|
completionSaga,
|
||||||
nextChallengeSaga,
|
nextChallengeSaga,
|
||||||
answerSaga,
|
answerSaga,
|
||||||
resetChallengeSaga
|
resetChallengeSaga,
|
||||||
|
bugSaga
|
||||||
];
|
];
|
||||||
|
@ -43,6 +43,7 @@ const initialState = {
|
|||||||
id: '',
|
id: '',
|
||||||
challenge: '',
|
challenge: '',
|
||||||
helpChatRoom: 'Help',
|
helpChatRoom: 'Help',
|
||||||
|
isBugOpen: false,
|
||||||
// old code storage key
|
// old code storage key
|
||||||
legacyKey: '',
|
legacyKey: '',
|
||||||
files: {},
|
files: {},
|
||||||
@ -185,7 +186,10 @@ const mainReducer = handleActions(
|
|||||||
isPressed: false,
|
isPressed: false,
|
||||||
delta: [ 0, 0 ],
|
delta: [ 0, 0 ],
|
||||||
mouse: [ userAnswer ? 1000 : -1000, 0]
|
mouse: [ userAnswer ? 1000 : -1000, 0]
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
[types.openBugModal]: state => ({ ...state, isBugOpen: true }),
|
||||||
|
[types.closeBugModal]: state => ({ ...state, isBugOpen: false })
|
||||||
},
|
},
|
||||||
initialState
|
initialState
|
||||||
);
|
);
|
||||||
|
@ -64,5 +64,11 @@ export default createTypes([
|
|||||||
'primeNextQuestion',
|
'primeNextQuestion',
|
||||||
'goToNextQuestion',
|
'goToNextQuestion',
|
||||||
'transitionVideo',
|
'transitionVideo',
|
||||||
'videoCompleted'
|
'videoCompleted',
|
||||||
|
|
||||||
|
// bug
|
||||||
|
'openBugModal',
|
||||||
|
'closeBugModal',
|
||||||
|
'openIssueSearch',
|
||||||
|
'createIssue'
|
||||||
], 'challenges');
|
], 'challenges');
|
||||||
|
Reference in New Issue
Block a user