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 checkpull/44443/head
parent
dd2ff1683c
commit
1c5d136add
|
@ -257,6 +257,7 @@ exports.createSchemaCustomization = ({ actions }) => {
|
|||
const typeDefs = `
|
||||
type ChallengeNode implements Node {
|
||||
challengeFiles: [FileContents]
|
||||
notes: String
|
||||
url: String
|
||||
}
|
||||
type FileContents {
|
||||
|
|
|
@ -287,6 +287,10 @@
|
|||
"info": "信息",
|
||||
"code": "編程",
|
||||
"tests": "測試",
|
||||
"restart": "Restart",
|
||||
"restart-step": "Restart Step",
|
||||
"console": "Console",
|
||||
"notes": "Notes",
|
||||
"preview": "預覽"
|
||||
},
|
||||
"help-translate": "我們仍然在翻譯以下證書。",
|
||||
|
|
|
@ -287,6 +287,10 @@
|
|||
"info": "信息",
|
||||
"code": "编程",
|
||||
"tests": "测试",
|
||||
"restart": "Restart",
|
||||
"restart-step": "Restart Step",
|
||||
"console": "Console",
|
||||
"notes": "Notes",
|
||||
"preview": "预览"
|
||||
},
|
||||
"help-translate": "我们仍然在翻译以下证书。",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -152,6 +152,7 @@ export type ChallengeNode = {
|
|||
owner: string;
|
||||
type: string;
|
||||
};
|
||||
notes: string;
|
||||
removeComments: boolean;
|
||||
isLocked: boolean;
|
||||
isPrivate: boolean;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -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', {
|
||||
|
|
|
@ -73,6 +73,10 @@ assert.equal(
|
|||
);
|
||||
```
|
||||
|
||||
# --notes--
|
||||
|
||||
Extra information for a challenge, in markdown
|
||||
|
||||
# --seed--
|
||||
|
||||
## --before-user-code--
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue