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 SidePanel from './Side-Panel.jsx';
|
||||
import Preview from './Preview.jsx';
|
||||
import BugModal from '../Bug-Modal.jsx';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
import {
|
||||
executeChallenge,
|
||||
@ -102,6 +103,7 @@ export class Challenge extends PureComponent {
|
||||
/>
|
||||
</Col>
|
||||
{ this.renderPreview(showPreview) }
|
||||
<BugModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -9,7 +9,11 @@ import TestSuite from './Test-Suite.jsx';
|
||||
import Output from './Output.jsx';
|
||||
import ToolPanel from './Tool-Panel.jsx';
|
||||
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 { toggleHelpChat } from '../../../../redux/actions';
|
||||
|
||||
@ -17,7 +21,8 @@ const bindableActions = {
|
||||
makeToast,
|
||||
executeChallenge,
|
||||
updateHint,
|
||||
toggleHelpChat
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
};
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
@ -59,7 +64,8 @@ export class SidePanel extends PureComponent {
|
||||
hints: PropTypes.string,
|
||||
updateHint: PropTypes.func,
|
||||
makeToast: PropTypes.func,
|
||||
toggleHelpChat: PropTypes.func
|
||||
toggleHelpChat: PropTypes.func,
|
||||
openBugModal: PropTypes.func
|
||||
};
|
||||
|
||||
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
||||
@ -99,7 +105,8 @@ export class SidePanel extends PureComponent {
|
||||
executeChallenge,
|
||||
updateHint,
|
||||
makeToast,
|
||||
toggleHelpChat
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
} = this.props;
|
||||
const style = {
|
||||
overflowX: 'hidden',
|
||||
@ -131,6 +138,7 @@ export class SidePanel extends PureComponent {
|
||||
executeChallenge={ executeChallenge }
|
||||
hint={ hint }
|
||||
makeToast={ makeToast }
|
||||
openBugModal={ openBugModal }
|
||||
toggleHelpChat={ toggleHelpChat }
|
||||
updateHint={ updateHint }
|
||||
/>
|
||||
|
@ -14,7 +14,8 @@ export default class ToolPanel extends PureComponent {
|
||||
executeChallenge: PropTypes.func,
|
||||
updateHint: PropTypes.func,
|
||||
hint: PropTypes.string,
|
||||
toggleHelpChat: PropTypes.func
|
||||
toggleHelpChat: PropTypes.func,
|
||||
openBugModal: PropTypes.func
|
||||
};
|
||||
|
||||
makeHint() {
|
||||
@ -54,7 +55,8 @@ export default class ToolPanel extends PureComponent {
|
||||
const {
|
||||
hint,
|
||||
executeChallenge,
|
||||
toggleHelpChat
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
@ -92,6 +94,7 @@ export default class ToolPanel extends PureComponent {
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
componentClass='label'
|
||||
onClick={ openBugModal }
|
||||
>
|
||||
Bug
|
||||
</Button>
|
||||
|
@ -7,6 +7,7 @@ import PureComponent from 'react-pure-render/component';
|
||||
import { Col } from 'react-bootstrap';
|
||||
import SidePanel from './Side-Panel.jsx';
|
||||
import ToolPanel from './Tool-Panel.jsx';
|
||||
import BugModal from '../Bug-Modal.jsx';
|
||||
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
|
||||
@ -71,6 +72,7 @@ export class Project extends PureComponent {
|
||||
<br />
|
||||
<ToolPanel />
|
||||
<br />
|
||||
<BugModal />
|
||||
</Col>
|
||||
</div>
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
BackEndForm
|
||||
} from './Forms.jsx';
|
||||
|
||||
import { submitChallenge } from '../../redux/actions';
|
||||
import { submitChallenge, openBugModal } from '../../redux/actions';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
import {
|
||||
simpleProject,
|
||||
@ -19,7 +19,8 @@ import { toggleHelpChat } from '../../../../redux/actions';
|
||||
|
||||
const bindableActions = {
|
||||
submitChallenge,
|
||||
toggleHelpChat
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
};
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
@ -43,7 +44,8 @@ export class ToolPanel extends PureComponent {
|
||||
isSimple: PropTypes.bool,
|
||||
isFrontEnd: PropTypes.bool,
|
||||
isSubmitting: PropTypes.bool,
|
||||
toggleHelpChat: PropTypes.func
|
||||
toggleHelpChat: PropTypes.func,
|
||||
openBugModal: PropTypes.func
|
||||
};
|
||||
|
||||
renderSubmitButton(isSignedIn, submitChallenge) {
|
||||
@ -69,7 +71,8 @@ export class ToolPanel extends PureComponent {
|
||||
isSignedIn,
|
||||
isSubmitting,
|
||||
submitChallenge,
|
||||
toggleHelpChat
|
||||
toggleHelpChat,
|
||||
openBugModal
|
||||
} = this.props;
|
||||
|
||||
const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
|
||||
@ -94,6 +97,7 @@ export class ToolPanel extends PureComponent {
|
||||
bsStyle='primary'
|
||||
className='btn-primary-ghost btn-big'
|
||||
componentClass='div'
|
||||
onClick={ openBugModal }
|
||||
>
|
||||
Bug
|
||||
</Button>
|
||||
|
@ -139,3 +139,9 @@ export const endShake = createAction(types.primeNextQuestion);
|
||||
|
||||
export const goToNextQuestion = createAction(types.goToNextQuestion);
|
||||
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 answerSaga from './answer-saga';
|
||||
import resetChallengeSaga from './reset-challenge-saga';
|
||||
import bugSaga from './bug-saga';
|
||||
|
||||
export * as actions from './actions';
|
||||
export reducer from './reducer';
|
||||
@ -15,5 +16,6 @@ export const sagas = [
|
||||
completionSaga,
|
||||
nextChallengeSaga,
|
||||
answerSaga,
|
||||
resetChallengeSaga
|
||||
resetChallengeSaga,
|
||||
bugSaga
|
||||
];
|
||||
|
@ -43,6 +43,7 @@ const initialState = {
|
||||
id: '',
|
||||
challenge: '',
|
||||
helpChatRoom: 'Help',
|
||||
isBugOpen: false,
|
||||
// old code storage key
|
||||
legacyKey: '',
|
||||
files: {},
|
||||
@ -185,7 +186,10 @@ const mainReducer = handleActions(
|
||||
isPressed: false,
|
||||
delta: [ 0, 0 ],
|
||||
mouse: [ userAnswer ? 1000 : -1000, 0]
|
||||
})
|
||||
}),
|
||||
|
||||
[types.openBugModal]: state => ({ ...state, isBugOpen: true }),
|
||||
[types.closeBugModal]: state => ({ ...state, isBugOpen: false })
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
@ -64,5 +64,11 @@ export default createTypes([
|
||||
'primeNextQuestion',
|
||||
'goToNextQuestion',
|
||||
'transitionVideo',
|
||||
'videoCompleted'
|
||||
'videoCompleted',
|
||||
|
||||
// bug
|
||||
'openBugModal',
|
||||
'closeBugModal',
|
||||
'openIssueSearch',
|
||||
'createIssue'
|
||||
], 'challenges');
|
||||
|
Reference in New Issue
Block a user