fix(client): forum help and hints (#36625)

* fix(client): add warning on exceeding forum post length limit

Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com>

* fix: corrected guide url for backend challenges and projects

* fix: moved  getGuideUrl to  utils

* fix: added forumTopicId to ChallengeNode PropTypes

* fix: conolidated helpers and add base url to env.json

* fix: made use template literals

* tests: created unit tests for getGuideUrl

* fix: add characters which will be encoded

Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* fix: use encoded version of title

Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* fix: encode title

Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
mrugesh
2019-08-28 21:55:56 +05:30
committed by GitHub
parent 428ab97650
commit a53a4ac303
7 changed files with 108 additions and 52 deletions

View File

@ -20,6 +20,7 @@ export const MarkdownRemark = PropTypes.shape({
export const ChallengeNode = PropTypes.shape({ export const ChallengeNode = PropTypes.shape({
block: PropTypes.string, block: PropTypes.string,
challengeOrder: PropTypes.number,
challengeType: PropTypes.number, challengeType: PropTypes.number,
dashedName: PropTypes.string, dashedName: PropTypes.string,
description: PropTypes.string, description: PropTypes.string,
@ -31,9 +32,9 @@ export const ChallengeNode = PropTypes.shape({
slug: PropTypes.string, slug: PropTypes.string,
blockName: PropTypes.string blockName: PropTypes.string
}), }),
forumTopicId: PropTypes.number,
guideUrl: PropTypes.string, guideUrl: PropTypes.string,
head: PropTypes.arrayOf(PropTypes.string), head: PropTypes.arrayOf(PropTypes.string),
challengeOrder: PropTypes.number,
instructions: PropTypes.string, instructions: PropTypes.string,
isBeta: PropTypes.bool, isBeta: PropTypes.bool,
isComingSoon: PropTypes.bool, isComingSoon: PropTypes.bool,

View File

@ -17,7 +17,7 @@ import {
updateProjectFormValues, updateProjectFormValues,
backendNS backendNS
} from '../redux'; } from '../redux';
import { createGuideUrl } from '../utils'; import { getGuideUrl } from '../utils';
import LearnLayout from '../../../components/layouts/Learn'; import LearnLayout from '../../../components/layouts/Learn';
import ChallengeTitle from '../components/Challenge-Title'; import ChallengeTitle from '../components/Challenge-Title';
@ -44,6 +44,7 @@ const propTypes = {
}), }),
description: PropTypes.string, description: PropTypes.string,
executeChallenge: PropTypes.func.isRequired, executeChallenge: PropTypes.func.isRequired,
forumTopicId: PropTypes.number,
id: PropTypes.string, id: PropTypes.string,
initConsole: PropTypes.func.isRequired, initConsole: PropTypes.func.isRequired,
initTests: PropTypes.func.isRequired, initTests: PropTypes.func.isRequired,
@ -155,8 +156,9 @@ export class BackEnd extends Component {
const { const {
data: { data: {
challengeNode: { challengeNode: {
fields: { blockName, slug }, fields: { blockName },
challengeType, challengeType,
forumTopicId,
title, title,
description, description,
instructions instructions
@ -210,7 +212,9 @@ export class BackEnd extends Component {
updateProjectForm={updateProjectFormValues} updateProjectForm={updateProjectFormValues}
/> />
)} )}
<ProjectToolPanel guideUrl={createGuideUrl(slug)} /> <ProjectToolPanel
guideUrl={getGuideUrl({ forumTopicId, title })}
/>
<br /> <br />
<Output <Output
defaultOutput={`/** defaultOutput={`/**
@ -247,6 +251,7 @@ export default connect(
export const query = graphql` export const query = graphql`
query BackendChallenge($slug: String!) { query BackendChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) { challengeNode(fields: { slug: { eq: $slug } }) {
forumTopicId
title title
description description
instructions instructions

View File

@ -20,7 +20,7 @@ import ResetModal from '../components/ResetModal';
import MobileLayout from './MobileLayout'; import MobileLayout from './MobileLayout';
import DesktopLayout from './DesktopLayout'; import DesktopLayout from './DesktopLayout';
import { createGuideUrl } from '../utils'; import { getGuideUrl } from '../utils';
import { challengeTypes } from '../../../../utils/challengeTypes'; import { challengeTypes } from '../../../../utils/challengeTypes';
import { ChallengeNode } from '../../../redux/propTypes'; import { ChallengeNode } from '../../../redux/propTypes';
import { dasherize } from '../../../../utils'; import { dasherize } from '../../../../utils';
@ -166,13 +166,6 @@ class ShowClassic extends Component {
return `${blockName}: ${title}`; return `${blockName}: ${title}`;
} }
getGuideUrl() {
const { forumTopicId, title } = this.getChallenge();
return forumTopicId
? 'https://www.freecodecamp.org/forum/t/' + forumTopicId
: createGuideUrl(title);
}
getVideoUrl = () => this.getChallenge().videoUrl; getVideoUrl = () => this.getChallenge().videoUrl;
getChallengeFile() { getChallengeFile() {
@ -200,11 +193,13 @@ class ShowClassic extends Component {
nextChallengePath, nextChallengePath,
prevChallengePath prevChallengePath
} = this.props.pageContext.challengeMeta; } = this.props.pageContext.challengeMeta;
const { forumTopicId, title } = this.getChallenge();
return ( return (
<SidePanel <SidePanel
className='full-height' className='full-height'
description={description} description={description}
guideUrl={this.getGuideUrl()} guideUrl={getGuideUrl({ forumTopicId, title })}
instructions={instructions} instructions={instructions}
introPath={introPath} introPath={introPath}
nextChallengePath={nextChallengePath} nextChallengePath={nextChallengePath}
@ -247,6 +242,7 @@ class ShowClassic extends Component {
} }
render() { render() {
const { forumTopicId, title } = this.getChallenge();
return ( return (
<LearnLayout> <LearnLayout>
<Helmet <Helmet
@ -255,7 +251,7 @@ class ShowClassic extends Component {
<Media maxWidth={MAX_MOBILE_WIDTH}> <Media maxWidth={MAX_MOBILE_WIDTH}>
<MobileLayout <MobileLayout
editor={this.renderEditor()} editor={this.renderEditor()}
guideUrl={this.getGuideUrl()} guideUrl={getGuideUrl({ forumTopicId, title })}
hasPreview={this.hasPreview()} hasPreview={this.hasPreview()}
instructions={this.renderInstructionsPanel({ instructions={this.renderInstructionsPanel({
showToolPanel: false showToolPanel: false

View File

@ -13,7 +13,7 @@ import {
updateProjectFormValues updateProjectFormValues
} from '../redux'; } from '../redux';
import { frontEndProject } from '../../../../utils/challengeTypes'; import { frontEndProject } from '../../../../utils/challengeTypes';
import { createGuideUrl } from '../utils'; import { getGuideUrl } from '../utils';
import LearnLayout from '../../../components/layouts/Learn'; import LearnLayout from '../../../components/layouts/Learn';
import Spacer from '../../../components/helpers/Spacer'; import Spacer from '../../../components/helpers/Spacer';
@ -93,7 +93,8 @@ export class Project extends Component {
data: { data: {
challengeNode: { challengeNode: {
challengeType, challengeType,
fields: { blockName, slug }, fields: { blockName },
forumTopicId,
title, title,
description, description,
guideUrl guideUrl
@ -127,7 +128,7 @@ export class Project extends Component {
onSubmit={openCompletionModal} onSubmit={openCompletionModal}
updateProjectForm={updateProjectFormValues} updateProjectForm={updateProjectFormValues}
/> />
<ToolPanel guideUrl={createGuideUrl(slug)} /> <ToolPanel guideUrl={getGuideUrl({ forumTopicId, title })} />
<Spacer /> <Spacer />
</div> </div>
<CompletionModal /> <CompletionModal />
@ -148,6 +149,7 @@ export default connect(
export const query = graphql` export const query = graphql`
query ProjectChallenge($slug: String!) { query ProjectChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) { challengeNode(fields: { slug: { eq: $slug } }) {
forumTopicId
title title
description description
challengeType challengeType

View File

@ -1,3 +1,4 @@
import dedent from 'dedent';
import { ofType } from 'redux-observable'; import { ofType } from 'redux-observable';
import { import {
types, types,
@ -7,6 +8,7 @@ import {
} from '../redux'; } from '../redux';
import { tap, mapTo } from 'rxjs/operators'; import { tap, mapTo } from 'rxjs/operators';
import { helpCategory } from '../../../../utils/challengeTypes'; import { helpCategory } from '../../../../utils/challengeTypes';
import { forumLocation } from '../../../../../config/env.json';
function filesToMarkdown(files = {}) { function filesToMarkdown(files = {}) {
const moreThenOneFile = Object.keys(files).length > 1; const moreThenOneFile = Object.keys(files).length > 1;
@ -17,17 +19,7 @@ function filesToMarkdown(files = {}) {
} }
const fileName = moreThenOneFile ? `\\ file: ${file.contents}` : ''; const fileName = moreThenOneFile ? `\\ file: ${file.contents}` : '';
const fileType = file.ext; const fileType = file.ext;
return ( return `${fileString}\`\`\`${fileType}\n${fileName}\n${file.contents}\n\`\`\`\n\n`;
fileString +
'```' +
fileType +
'\n' +
fileName +
'\n' +
file.contents +
'\n' +
'```\n\n'
);
}, '\n'); }, '\n');
} }
@ -44,28 +36,59 @@ function createQuestionEpic(action$, state$, { window }) {
navigator: { userAgent }, navigator: { userAgent },
location: { href } location: { href }
} = window; } = window;
const textMessage = [
"**Tell us what's happening:**\n\n\n\n", const endingText = dedent(
'**Your code so far**\n', `**Your browser information:**
filesToMarkdown(files),
'**Your browser information:**\n\n', User Agent is: <code>${userAgent}</code>.
'User Agent is: `',
userAgent, **Challenge:** ${challengeTitle}
'`.\n\n',
'**Challenge:**\n', **Link to the challenge:**
challengeTitle, ${href}`
'\n**Link to the challenge:**\n',
href
].join('');
window.open(
'https://forum.freecodecamp.org/new-topic' +
'?category=' +
window.encodeURIComponent(helpCategory[challengeType] || 'Help') +
'&title=' +
'&body=' +
window.encodeURIComponent(textMessage),
'_blank'
); );
let textMessage = dedent(
`**Tell us what's happening:**\n\n\n\n**Your code so far**
${filesToMarkdown(files)}\n${endingText}`
);
const altTextMessage = dedent(
`**Tell us what's happening:**
**Your code so far**
WARNING
The challenge seed code and/or your solution exceeded the maximum length we can port over from the challenge.
You will need to take an additional step here so the code you wrote presents in an easy to read format.
Please copy/paste all the editor code showing in the challenge from where you just linked.
\`\`\`
Replace these two sentences with your copied code.
Please leave the \`\`\` line above and the \`\`\` line below,
because they allow your code to properly format in the post.
\`\`\`\n${endingText}`
);
const category = window.encodeURIComponent(
helpCategory[challengeType] || 'Help'
);
const studentCode = window.encodeURIComponent(textMessage);
const altStudentCode = window.encodeURIComponent(altTextMessage);
const baseURI = `${forumLocation}/new-topic?category=${category}&title=&body=`;
const defaultURI = `${baseURI}${studentCode}`;
const altURI = `${baseURI}${altStudentCode}`;
window.open(defaultURI.length < 8000 ? defaultURI : altURI, '_blank');
}), }),
mapTo(closeModal('help')) mapTo(closeModal('help'))
); );

View File

@ -1,7 +1,10 @@
const guideBase = 'https://www.freecodecamp.org/forum/search?q='; import { forumLocation } from '../../../../../config/env.json';
export function createGuideUrl(title = '') { export function getGuideUrl({ forumTopicId, title = '' }) {
return guideBase + title + '%20in%3Atitle%20order%3Aviews'; title = encodeURIComponent(title);
return forumTopicId
? `${forumLocation}/t/${forumTopicId}`
: `${forumLocation}/search?q=${title}%20in%3Atitle%20order%3Aviews`;
} }
export function isGoodXHRStatus(status) { export function isGoodXHRStatus(status) {

View File

@ -0,0 +1,26 @@
/* global expect */
import { forumLocation } from '../../../../../config/env.json';
const { getGuideUrl } = require('./');
describe('index', () => {
describe('getGuideUrl', () => {
it('should use forum topic url when forumTopicId is supplied', () => {
const value = getGuideUrl({
forumTopicId: 12345,
title: 'a sample title'
});
expect(value).toEqual(`${forumLocation}/t/12345`);
});
it('should use search endpoint when no forumTopicId is supplied', () => {
const value = getGuideUrl({
title: '& a sample title?'
});
expect(value).toEqual(
`${forumLocation}/search?q=%26%20a%20sample%20title%3F%20in%3Atitle%20order%3Aviews`
);
});
});
});