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
pull/44443/head
Tom 2021-12-09 12:42:03 -06:00 committed by GitHub
parent dd2ff1683c
commit 1c5d136add
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 141 additions and 26 deletions

View File

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

View File

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

View File

@ -287,6 +287,10 @@
"info": "信息",
"code": "编程",
"tests": "测试",
"restart": "Restart",
"restart-step": "Restart Step",
"console": "Console",
"notes": "Notes",
"preview": "预览"
},
"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": "We are still translating the following certifications.",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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