feat(client): add notes tab to project based curriculum (#44247)

* feat: add notes tab to project based curriculum

* feat: add console key to i18n

* feat: add reset to i18n

* fix: use translations in action-row

* fix: use hasEditableBoundaries as check for when to display instructions/editor-tabs

* fix: clean up notes components and use prism formatting

* feat: add notes to docs/how-to-work-on-challenges

* revert: unused code

* fix: lint errors?

* fix: lint errors

* fix: add notes to graphql schema

* fix: add notes to challenge schema

* fix: only display notes on project based

* fix: add env data back to mobile layout

* fix: prettify

* revert: notes

* fix: hide notes on mobile for non project based

* rename: switchDisplayTab -> togglePane

* revert: hasEditableBoundaries check back to projectBasedChallenge check
This commit is contained in:
Tom
2021-12-09 12:42:03 -06:00
committed by GitHub
parent dd2ff1683c
commit 1c5d136add
16 changed files with 141 additions and 26 deletions

View File

@ -257,6 +257,7 @@ exports.createSchemaCustomization = ({ actions }) => {
const typeDefs = ` const typeDefs = `
type ChallengeNode implements Node { type ChallengeNode implements Node {
challengeFiles: [FileContents] challengeFiles: [FileContents]
notes: String
url: String url: String
} }
type FileContents { type FileContents {

View File

@ -287,6 +287,10 @@
"info": "信息", "info": "信息",
"code": "編程", "code": "編程",
"tests": "測試", "tests": "測試",
"restart": "Restart",
"restart-step": "Restart Step",
"console": "Console",
"notes": "Notes",
"preview": "預覽" "preview": "預覽"
}, },
"help-translate": "我們仍然在翻譯以下證書。", "help-translate": "我們仍然在翻譯以下證書。",

View File

@ -287,6 +287,10 @@
"info": "信息", "info": "信息",
"code": "编程", "code": "编程",
"tests": "测试", "tests": "测试",
"restart": "Restart",
"restart-step": "Restart Step",
"console": "Console",
"notes": "Notes",
"preview": "预览" "preview": "预览"
}, },
"help-translate": "我们仍然在翻译以下证书。", "help-translate": "我们仍然在翻译以下证书。",

View File

@ -287,6 +287,10 @@
"info": "Info", "info": "Info",
"code": "Code", "code": "Code",
"tests": "Tests", "tests": "Tests",
"restart": "Restart",
"restart-step": "Restart Step",
"console": "Console",
"notes": "Notes",
"preview": "Preview" "preview": "Preview"
}, },
"help-translate": "We are still translating the following certifications.", "help-translate": "We are still translating the following certifications.",

View File

@ -287,6 +287,10 @@
"info": "Info", "info": "Info",
"code": "Código", "code": "Código",
"tests": "Pruebas", "tests": "Pruebas",
"restart": "Restart",
"restart-step": "Restart Step",
"console": "Console",
"notes": "Notes",
"preview": "Vista" "preview": "Vista"
}, },
"help-translate": "Todavía estamos traduciendo las siguientes certificaciones.", "help-translate": "Todavía estamos traduciendo las siguientes certificaciones.",

View File

@ -287,6 +287,10 @@
"info": "Informazioni", "info": "Informazioni",
"code": "Codice", "code": "Codice",
"tests": "Test", "tests": "Test",
"restart": "Restart",
"restart-step": "Restart Step",
"console": "Console",
"notes": "Notes",
"preview": "Anteprima" "preview": "Anteprima"
}, },
"help-translate": "Stiamo ancora traducendo le seguenti certificazioni.", "help-translate": "Stiamo ancora traducendo le seguenti certificazioni.",

View File

@ -287,6 +287,10 @@
"info": "Informações", "info": "Informações",
"code": "Código", "code": "Código",
"tests": "Testes", "tests": "Testes",
"restart": "Restart",
"restart-step": "Restart Step",
"console": "Console",
"notes": "Notes",
"preview": "Pré-visualizar" "preview": "Pré-visualizar"
}, },
"help-translate": "Ainda estamos traduzindo as certificações a seguir.", "help-translate": "Ainda estamos traduzindo as certificações a seguir.",

View File

@ -152,6 +152,7 @@ export type ChallengeNode = {
owner: string; owner: string;
type: string; type: string;
}; };
notes: string;
removeComments: boolean; removeComments: boolean;
isLocked: boolean; isLocked: boolean;
isPrivate: boolean; isPrivate: boolean;

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import BreadCrumb from '../components/bread-crumb'; import BreadCrumb from '../components/bread-crumb';
import { resetChallenge } from '../redux'; import { resetChallenge } from '../redux';
@ -6,11 +7,12 @@ import EditorTabs from './editor-tabs';
interface ActionRowProps { interface ActionRowProps {
block: string; block: string;
hasNotes: boolean;
showConsole: boolean; showConsole: boolean;
showNotes?: boolean; showNotes: boolean;
showPreview: boolean; showPreview: boolean;
superBlock: string; superBlock: string;
switchDisplayTab: (displayTab: string) => void; togglePane: (pane: string) => void;
resetChallenge: () => void; resetChallenge: () => void;
} }
@ -19,13 +21,16 @@ const mapDispatchToProps = {
}; };
const ActionRow = ({ const ActionRow = ({
switchDisplayTab, hasNotes,
togglePane,
showNotes,
showPreview, showPreview,
showConsole, showConsole,
superBlock, superBlock,
block, block,
resetChallenge resetChallenge
}: ActionRowProps): JSX.Element => { }: ActionRowProps): JSX.Element => {
const { t } = useTranslation();
return ( return (
<div className='action-row'> <div className='action-row'>
<div className='breadcrumbs-demo'> <div className='breadcrumbs-demo'>
@ -38,22 +43,31 @@ const ActionRow = ({
onClick={resetChallenge} onClick={resetChallenge}
role='tab' role='tab'
> >
Restart Step {t('learn.editor-tabs.restart-step')}
</button> </button>
<div className='panel-display-tabs'> <div className='panel-display-tabs'>
<button <button
className={showConsole ? 'active-tab' : ''} className={showConsole ? 'active-tab' : ''}
onClick={() => switchDisplayTab('showConsole')} onClick={() => togglePane('showConsole')}
role='tab' role='tab'
> >
JS Console {t('learn.editor-tabs.console')}
</button> </button>
{hasNotes && (
<button
className={showNotes ? 'active-tab' : ''}
onClick={() => togglePane('showNotes')}
role='tab'
>
{t('learn.editor-tabs.notes')}
</button>
)}
<button <button
className={showPreview ? 'active-tab' : ''} className={showPreview ? 'active-tab' : ''}
onClick={() => switchDisplayTab('showPreview')} onClick={() => togglePane('showPreview')}
role='tab' role='tab'
> >
Show Preview {t('learn.editor-tabs.preview')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
import { first } from 'lodash-es'; import { first } from 'lodash-es';
import React, { useState, ReactElement } from 'react'; import React, { useState, ReactElement } from 'react';
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
import envData from '../../../../../config/env.json';
import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles'; import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles';
import envData from '../../../../../config/env.json';
import { import {
ChallengeFile, ChallengeFile,
ChallengeFiles, ChallengeFiles,
@ -19,15 +19,18 @@ interface DesktopLayoutProps {
challengeFiles: ChallengeFiles; challengeFiles: ChallengeFiles;
editor: ReactElement | null; editor: ReactElement | null;
hasEditableBoundaries: boolean; hasEditableBoundaries: boolean;
hasNotes: boolean;
hasPreview: boolean; hasPreview: boolean;
instructions: ReactElement; instructions: ReactElement;
layoutState: { layoutState: {
codePane: Pane; codePane: Pane;
editorPane: Pane; editorPane: Pane;
instructionPane: Pane; instructionPane: Pane;
notesPane: Pane;
previewPane: Pane; previewPane: Pane;
testsPane: Pane; testsPane: Pane;
}; };
notes: ReactElement;
preview: ReactElement; preview: ReactElement;
resizeProps: ResizeProps; resizeProps: ResizeProps;
superBlock: string; superBlock: string;
@ -43,8 +46,8 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
const [showPreview, setShowPreview] = useState(true); const [showPreview, setShowPreview] = useState(true);
const [showConsole, setShowConsole] = useState(false); const [showConsole, setShowConsole] = useState(false);
const switchDisplayTab = (displayTab: string): void => { const togglePane = (pane: string): void => {
switch (displayTab) { switch (pane) {
case 'showPreview': case 'showPreview':
setShowPreview(!showPreview); setShowPreview(!showPreview);
break; break;
@ -72,8 +75,10 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
instructions, instructions,
editor, editor,
testOutput, testOutput,
hasNotes,
hasPreview, hasPreview,
layoutState, layoutState,
notes,
preview, preview,
hasEditableBoundaries, hasEditableBoundaries,
superBlock superBlock
@ -84,20 +89,28 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
const displayPreview = projectBasedChallenge const displayPreview = projectBasedChallenge
? showPreview && hasPreview ? showPreview && hasPreview
: hasPreview; : hasPreview;
const displayNotes = projectBasedChallenge ? showNotes && hasNotes : false;
const displayConsole = projectBasedChallenge ? showConsole : true; const displayConsole = projectBasedChallenge ? showConsole : true;
const { codePane, editorPane, instructionPane, previewPane, testsPane } = const {
layoutState; codePane,
editorPane,
instructionPane,
notesPane,
previewPane,
testsPane
} = layoutState;
return ( return (
<div className='desktop-layout'> <div className='desktop-layout'>
{projectBasedChallenge && ( {projectBasedChallenge && (
<ActionRow <ActionRow
block={block} block={block}
hasNotes={hasNotes}
showConsole={showConsole} showConsole={showConsole}
showNotes={showNotes} showNotes={showNotes}
showPreview={showPreview} showPreview={showPreview}
superBlock={superBlock} superBlock={superBlock}
switchDisplayTab={switchDisplayTab} togglePane={togglePane}
/> />
)} )}
<ReflexContainer orientation='vertical'> <ReflexContainer orientation='vertical'>
@ -138,6 +151,13 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
</ReflexContainer> </ReflexContainer>
)} )}
</ReflexElement> </ReflexElement>
{displayNotes && <ReflexSplitter propagate={true} {...resizeProps} />}
{displayNotes && (
<ReflexElement flex={notesPane.flex} {...resizeProps}>
{notes}
</ReflexElement>
)}
{displayPreview && <ReflexSplitter propagate={true} {...resizeProps} />} {displayPreview && <ReflexSplitter propagate={true} {...resizeProps} />}
{displayPreview && ( {displayPreview && (
<ReflexElement flex={previewPane.flex} {...resizeProps}> <ReflexElement flex={previewPane.flex} {...resizeProps}>

View File

@ -1,6 +1,6 @@
import { TabPane, Tabs } from '@freecodecamp/react-bootstrap'; import { TabPane, Tabs } from '@freecodecamp/react-bootstrap';
import i18next from 'i18next'; import i18next from 'i18next';
import React, { Component } from 'react'; import React, { Component, ReactElement } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
@ -27,9 +27,12 @@ interface MobileLayoutProps {
currentTab: number; currentTab: number;
editor: JSX.Element | null; editor: JSX.Element | null;
guideUrl: string; guideUrl: string;
hasEditableBoundaries: boolean;
hasNotes: boolean;
hasPreview: boolean; hasPreview: boolean;
instructions: JSX.Element; instructions: JSX.Element;
moveToTab: typeof moveToTab; moveToTab: typeof moveToTab;
notes: ReactElement;
preview: JSX.Element; preview: JSX.Element;
testOutput: JSX.Element; testOutput: JSX.Element;
videoUrl: string; videoUrl: string;
@ -44,10 +47,13 @@ class MobileLayout extends Component<MobileLayoutProps> {
const { const {
currentTab, currentTab,
moveToTab, moveToTab,
hasEditableBoundaries,
instructions, instructions,
editor, editor,
testOutput, testOutput,
hasNotes,
hasPreview, hasPreview,
notes,
preview, preview,
guideUrl, guideUrl,
videoUrl, videoUrl,
@ -61,7 +67,9 @@ class MobileLayout extends Component<MobileLayoutProps> {
// Unlike the desktop layout the mobile version does not have an ActionRow, // Unlike the desktop layout the mobile version does not have an ActionRow,
// but still needs a way to switch between the different tabs. // but still needs a way to switch between the different tabs.
const displayEditorTabs = showUpcomingChanges && usesMultifileEditor; const projectBasedChallenge = showUpcomingChanges && usesMultifileEditor;
const eventKeys = [1, 2, 3, 4, 5];
return ( return (
<> <>
@ -71,27 +79,40 @@ class MobileLayout extends Component<MobileLayoutProps> {
id='challenge-page-tabs' id='challenge-page-tabs'
onSelect={moveToTab} onSelect={moveToTab}
> >
<TabPane eventKey={1} title={i18next.t('learn.editor-tabs.info')}> {!hasEditableBoundaries && (
{instructions} <TabPane
</TabPane> eventKey={eventKeys.shift()}
title={i18next.t('learn.editor-tabs.info')}
>
{instructions}
</TabPane>
)}
<TabPane <TabPane
eventKey={2} eventKey={eventKeys.shift()}
title={i18next.t('learn.editor-tabs.code')} title={i18next.t('learn.editor-tabs.code')}
{...editorTabPaneProps} {...editorTabPaneProps}
> >
{displayEditorTabs && <EditorTabs />} {projectBasedChallenge && <EditorTabs />}
{editor} {editor}
</TabPane> </TabPane>
<TabPane <TabPane
eventKey={3} eventKey={eventKeys.shift()}
title={i18next.t('learn.editor-tabs.tests')} title={i18next.t('learn.editor-tabs.tests')}
{...editorTabPaneProps} {...editorTabPaneProps}
> >
{testOutput} {testOutput}
</TabPane> </TabPane>
{hasNotes && projectBasedChallenge && (
<TabPane
eventKey={eventKeys.shift()}
title={i18next.t('learn.editor-tabs.notes')}
>
{notes}
</TabPane>
)}
{hasPreview && ( {hasPreview && (
<TabPane <TabPane
eventKey={4} eventKey={eventKeys.shift()}
title={i18next.t('learn.editor-tabs.preview')} title={i18next.t('learn.editor-tabs.preview')}
> >
{preview} {preview}

View File

@ -9,8 +9,8 @@ import { bindActionCreators, Dispatch } from 'redux';
import { createStructuredSelector } from 'reselect'; import { createStructuredSelector } from 'reselect';
import store from 'store'; import store from 'store';
import { challengeTypes } from '../../../../utils/challenge-types'; import { challengeTypes } from '../../../../utils/challenge-types';
import LearnLayout from '../../../components/layouts/learn'; import LearnLayout from '../../../components/layouts/learn';
import { import {
ChallengeFile, ChallengeFile,
ChallengeFiles, ChallengeFiles,
@ -26,6 +26,7 @@ import ResetModal from '../components/ResetModal';
import ChallengeTitle from '../components/challenge-title'; import ChallengeTitle from '../components/challenge-title';
import CompletionModal from '../components/completion-modal'; import CompletionModal from '../components/completion-modal';
import HelpModal from '../components/help-modal'; import HelpModal from '../components/help-modal';
import Notes from '../components/notes';
import Output from '../components/output'; import Output from '../components/output';
import Preview from '../components/preview'; import Preview from '../components/preview';
import ProjectPreviewModal, { import ProjectPreviewModal, {
@ -115,6 +116,7 @@ interface ReflexLayout {
codePane: { flex: number }; codePane: { flex: number };
editorPane: { flex: number }; editorPane: { flex: number };
instructionPane: { flex: number }; instructionPane: { flex: number };
notesPane: { flex: number };
previewPane: { flex: number }; previewPane: { flex: number };
testsPane: { flex: number }; testsPane: { flex: number };
} }
@ -126,6 +128,7 @@ const BASE_LAYOUT = {
editorPane: { flex: 1 }, editorPane: { flex: 1 },
instructionPane: { flex: 1 }, instructionPane: { flex: 1 },
previewPane: { flex: 0.7 }, previewPane: { flex: 0.7 },
notesPane: { flex: 0.7 },
testsPane: { flex: 0.25 } testsPane: { flex: 0.25 }
}; };
@ -371,6 +374,10 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
); );
} }
renderNotes(notes?: string) {
return <Notes notes={notes} />;
}
renderPreview() { renderPreview() {
return ( return (
<Preview <Preview
@ -397,7 +404,8 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
forumTopicId, forumTopicId,
superBlock, superBlock,
title, title,
usesMultifileEditor usesMultifileEditor,
notes
} = this.getChallenge(); } = this.getChallenge();
const { const {
executeChallenge, executeChallenge,
@ -425,10 +433,13 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
<MobileLayout <MobileLayout
editor={this.renderEditor()} editor={this.renderEditor()}
guideUrl={getGuideUrl({ forumTopicId, title })} guideUrl={getGuideUrl({ forumTopicId, title })}
hasEditableBoundaries={this.hasEditableBoundaries()}
hasNotes={!!notes}
hasPreview={this.hasPreview()} hasPreview={this.hasPreview()}
instructions={this.renderInstructionsPanel({ instructions={this.renderInstructionsPanel({
showToolPanel: false showToolPanel: false
})} })}
notes={this.renderNotes(notes)}
preview={this.renderPreview()} preview={this.renderPreview()}
testOutput={this.renderTestOutput()} testOutput={this.renderTestOutput()}
usesMultifileEditor={usesMultifileEditor} usesMultifileEditor={usesMultifileEditor}
@ -441,11 +452,13 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
challengeFiles={challengeFiles} challengeFiles={challengeFiles}
editor={this.renderEditor()} editor={this.renderEditor()}
hasEditableBoundaries={this.hasEditableBoundaries()} hasEditableBoundaries={this.hasEditableBoundaries()}
hasNotes={!!notes}
hasPreview={this.hasPreview()} hasPreview={this.hasPreview()}
instructions={this.renderInstructionsPanel({ instructions={this.renderInstructionsPanel({
showToolPanel: true showToolPanel: true
})} })}
layoutState={this.state.layout} layoutState={this.state.layout}
notes={this.renderNotes(notes)}
preview={this.renderPreview()} preview={this.renderPreview()}
resizeProps={this.resizeProps} resizeProps={this.resizeProps}
superBlock={superBlock} superBlock={superBlock}
@ -481,6 +494,7 @@ export const query = graphql`
title title
description description
instructions instructions
notes
removeComments removeComments
challengeType challengeType
helpCategory helpCategory

View File

@ -0,0 +1,14 @@
import React from 'react';
import PrismFormatted from './prism-formatted';
interface NotesProps {
notes?: string;
}
function Notes({ notes }: NotesProps): JSX.Element {
return <>{notes && <PrismFormatted text={notes} />}</>;
}
Notes.displayName = 'Notes';
export default Notes;

View File

@ -52,6 +52,7 @@ const schema = Joi.object()
isComingSoon: Joi.bool(), isComingSoon: Joi.bool(),
isLocked: Joi.bool(), isLocked: Joi.bool(),
isPrivate: Joi.bool(), isPrivate: Joi.bool(),
notes: Joi.string().allow(''),
order: Joi.number(), order: Joi.number(),
// video challenges only: // video challenges only:
videoId: Joi.when('challengeType', { videoId: Joi.when('challengeType', {

View File

@ -73,6 +73,10 @@ assert.equal(
); );
``` ```
# --notes--
Extra information for a challenge, in markdown
# --seed-- # --seed--
## --before-user-code-- ## --before-user-code--

View File

@ -45,12 +45,13 @@ const processor = unified()
.use(restoreDirectives) .use(restoreDirectives)
.use(addVideoQuestion) .use(addVideoQuestion)
.use(addTests) .use(addTests)
.use(addText, ['description', 'instructions']); .use(addText, ['description', 'instructions', 'notes']);
exports.parseMD = function parseMD(filename) { exports.parseMD = function parseMD(filename) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const file = readSync(filename); const file = readSync(filename);
const tree = processor.parse(file); const tree = processor.parse(file);
processor.run(tree, file, function (err, node, file) { processor.run(tree, file, function (err, node, file) {
if (!err) { if (!err) {
resolve(file.data); resolve(file.data);