fix: stop modal appearing in steps (#43728)

* fix: stop showing completion modal on steps

* feat: submit steps with ctrl+enter

* fix: handle ctrl+enter when not focussing editor

* fix: reset tests when user types

* refactor: pass showCompletionModal as an option

Otherwise we have to write executeChallenge(true) which does not mean
what you might reasonably expect.

* fix: always executeChallenge when not on step

* fix: update frontend project show

* fix: handle missing payload

* refactor: isProjectStep -> hasEditableRegion

* refactor: more renaming

* fix: make meta.json control multifile editor use

* fix: update the challengeSchema correctly

* Update client/src/templates/Challenges/classic/editor.tsx

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>

* fix: remove logging

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
Oliver Eyton-Williams
2021-10-13 13:47:59 +02:00
committed by GitHub
parent 9220bfedad
commit 22afdd1aad
24 changed files with 149 additions and 45 deletions

View File

@ -223,6 +223,7 @@ export type ChallengeNodeType = {
title: string; title: string;
translationPending: boolean; translationPending: boolean;
url: string; url: string;
usesMultifileEditor: boolean;
videoId: string; videoId: string;
videoLocaleIds?: VideoLocaleIds; videoLocaleIds?: VideoLocaleIds;
bilibiliIds?: BilibiliIds; bilibiliIds?: BilibiliIds;
@ -436,6 +437,7 @@ export type ChallengeFile = {
ext: ExtTypes; ext: ExtTypes;
name: string; name: string;
editableRegionBoundaries: number[]; editableRegionBoundaries: number[];
usesMultifileEditor: boolean;
path: string; path: string;
error: null | string; error: null | string;
head: string; head: string;

View File

@ -31,6 +31,7 @@ const propTypes = {
fileKey: PropTypes.string, fileKey: PropTypes.string,
initialEditorContent: PropTypes.string, initialEditorContent: PropTypes.string,
initialExt: PropTypes.string, initialExt: PropTypes.string,
initialTests: PropTypes.array,
output: PropTypes.arrayOf(PropTypes.string), output: PropTypes.arrayOf(PropTypes.string),
resizeProps: PropTypes.shape({ resizeProps: PropTypes.shape({
onStopResize: PropTypes.func, onStopResize: PropTypes.func,
@ -42,6 +43,7 @@ const propTypes = {
// TODO: is this used? // TODO: is this used?
title: PropTypes.string, title: PropTypes.string,
updateFile: PropTypes.func.isRequired, updateFile: PropTypes.func.isRequired,
usesMultifileEditor: PropTypes.bool,
visibleEditors: PropTypes.shape({ visibleEditors: PropTypes.shape({
indexjs: PropTypes.bool, indexjs: PropTypes.bool,
indexjsx: PropTypes.bool, indexjsx: PropTypes.bool,
@ -84,10 +86,12 @@ class MultifileEditor extends Component {
containerRef, containerRef,
description, description,
editorRef, editorRef,
initialTests,
theme, theme,
resizeProps, resizeProps,
title, title,
visibleEditors: { indexcss, indexhtml, indexjs, indexjsx } visibleEditors: { indexcss, indexhtml, indexjs, indexjsx },
usesMultifileEditor
} = this.props; } = this.props;
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom'; const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
// TODO: the tabs mess up the rendering (scroll doesn't work properly and // TODO: the tabs mess up the rendering (scroll doesn't work properly and
@ -139,10 +143,12 @@ class MultifileEditor extends Component {
description={targetEditor === 'indexjsx' ? description : null} description={targetEditor === 'indexjsx' ? description : null}
editorRef={editorRef} editorRef={editorRef}
fileKey='indexjsx' fileKey='indexjsx'
initialTests={initialTests}
key='indexjsx' key='indexjsx'
resizeProps={resizeProps} resizeProps={resizeProps}
theme={editorTheme} theme={editorTheme}
title={title} title={title}
usesMultifileEditor={usesMultifileEditor}
/> />
</ReflexElement> </ReflexElement>
)} )}
@ -159,10 +165,12 @@ class MultifileEditor extends Component {
} }
editorRef={editorRef} editorRef={editorRef}
fileKey='indexhtml' fileKey='indexhtml'
initialTests={initialTests}
key='indexhtml' key='indexhtml'
resizeProps={resizeProps} resizeProps={resizeProps}
theme={editorTheme} theme={editorTheme}
title={title} title={title}
usesMultifileEditor={usesMultifileEditor}
/> />
</ReflexElement> </ReflexElement>
)} )}
@ -177,10 +185,12 @@ class MultifileEditor extends Component {
description={targetEditor === 'indexcss' ? description : null} description={targetEditor === 'indexcss' ? description : null}
editorRef={editorRef} editorRef={editorRef}
fileKey='indexcss' fileKey='indexcss'
initialTests={initialTests}
key='indexcss' key='indexcss'
resizeProps={resizeProps} resizeProps={resizeProps}
theme={editorTheme} theme={editorTheme}
title={title} title={title}
usesMultifileEditor={usesMultifileEditor}
/> />
</ReflexElement> </ReflexElement>
)} )}
@ -196,10 +206,12 @@ class MultifileEditor extends Component {
description={targetEditor === 'indexjs' ? description : null} description={targetEditor === 'indexjs' ? description : null}
editorRef={editorRef} editorRef={editorRef}
fileKey='indexjs' fileKey='indexjs'
initialTests={initialTests}
key='indexjs' key='indexjs'
resizeProps={resizeProps} resizeProps={resizeProps}
theme={editorTheme} theme={editorTheme}
title={title} title={title}
usesMultifileEditor={usesMultifileEditor}
/> />
</ReflexElement> </ReflexElement>
)} )}

View File

@ -37,7 +37,8 @@ import {
setEditorFocusability, setEditorFocusability,
updateFile, updateFile,
challengeTestsSelector, challengeTestsSelector,
submitChallenge submitChallenge,
initTests
} from '../redux'; } from '../redux';
import './editor.css'; import './editor.css';
@ -52,11 +53,14 @@ interface EditorProps {
description: string; description: string;
dimensions: DimensionsType; dimensions: DimensionsType;
editorRef: MutableRefObject<editor.IStandaloneCodeEditor>; editorRef: MutableRefObject<editor.IStandaloneCodeEditor>;
executeChallenge: (isShouldCompletionModalOpen?: boolean) => void; executeChallenge: (options?: { showCompletionModal: boolean }) => void;
ext: ExtTypes; ext: ExtTypes;
fileKey: FileKeyTypes; fileKey: FileKeyTypes;
initialEditorContent: string; initialEditorContent: string;
initialExt: string; initialExt: string;
initTests: (tests: Test[]) => void;
initialTests: Test[];
isProjectStep: boolean;
output: string[]; output: string[];
resizeProps: ResizePropsType; resizeProps: ResizePropsType;
saveEditorContent: () => void; saveEditorContent: () => void;
@ -70,6 +74,7 @@ interface EditorProps {
editorValue: string; editorValue: string;
editableRegionBoundaries: number[] | null; editableRegionBoundaries: number[] | null;
}) => void; }) => void;
usesMultifileEditor: boolean;
} }
interface EditorProperties { interface EditorProperties {
@ -122,7 +127,8 @@ const mapDispatchToProps = {
saveEditorContent, saveEditorContent,
setEditorFocusability, setEditorFocusability,
updateFile, updateFile,
submitChallenge submitChallenge,
initTests
}; };
const modeMap = { const modeMap = {
@ -181,7 +187,7 @@ const initialData: EditorProperties = {
}; };
const Editor = (props: EditorProps): JSX.Element => { const Editor = (props: EditorProps): JSX.Element => {
const { editorRef, fileKey } = props; const { editorRef, fileKey, initTests } = props;
// These refs are used during initialisation of the editor as well as by // These refs are used during initialisation of the editor as well as by
// callbacks. Since they have to be initialised before editorWillMount and // callbacks. Since they have to be initialised before editorWillMount and
// editorDidMount are called, we cannot use useState. Reason being that will // editorDidMount are called, we cannot use useState. Reason being that will
@ -198,6 +204,11 @@ const Editor = (props: EditorProps): JSX.Element => {
}); });
const data = dataRef.current[fileKey]; const data = dataRef.current[fileKey];
// since editorDidMount runs once with the initial props object, it keeps a
// reference to *those* props. If we want it to use the latest props, we can
// use a ref, since it will be updated on every render.
const testRef = useRef<Test[]>([]);
testRef.current = props.tests;
// TENATIVE PLAN: create a typical order [html/jsx, css, js], put the // TENATIVE PLAN: create a typical order [html/jsx, css, js], put the
// available files into that order. i.e. if it's just one file it will // available files into that order. i.e. if it's just one file it will
@ -294,7 +305,7 @@ const Editor = (props: EditorProps): JSX.Element => {
data.editor = editor; data.editor = editor;
if (hasEditableRegion()) { if (hasEditableRegion()) {
initializeProjectStepFeatures(); initializeDescriptionAndOutputWidgets();
addContentChangeListener(); addContentChangeListener();
showEditableRegion(editor); showEditableRegion(editor);
} }
@ -342,8 +353,17 @@ const Editor = (props: EditorProps): JSX.Element => {
label: 'Run tests', label: 'Run tests',
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
// TODO: Discuss with Ahmad what should pop-up when a challenge is completed run: () => {
run: () => props.executeChallenge(true) if (props.usesMultifileEditor) {
if (challengeIsComplete()) {
props.submitChallenge();
} else {
props.executeChallenge();
}
} else {
props.executeChallenge({ showCompletionModal: true });
}
}
}); });
editor.addAction({ editor.addAction({
id: 'leave-editor', id: 'leave-editor',
@ -648,7 +668,7 @@ const Editor = (props: EditorProps): JSX.Element => {
} }
}; };
function initializeProjectStepFeatures() { function initializeDescriptionAndOutputWidgets() {
const editor = data.editor; const editor = data.editor;
if (editor) { if (editor) {
initializeRegions(getEditableRegionFromRedux()); initializeRegions(getEditableRegionFromRedux());
@ -955,6 +975,17 @@ const Editor = (props: EditorProps): JSX.Element => {
.setEndPosition(range.endLineNumber, endColumnText.length + 2); .setEndPosition(range.endLineNumber, endColumnText.length + 2);
} }
function challengeIsComplete() {
const tests = testRef.current;
return tests.every(test => test.pass && !test.err);
}
function challengeHasErrors() {
const tests = testRef.current;
return tests.some(test => test.err);
}
// runs every update to the editor and when the challenge is reset
useEffect(() => { useEffect(() => {
// If a challenge is reset, it needs to communicate that change to the // If a challenge is reset, it needs to communicate that change to the
// editor. // editor.
@ -962,13 +993,15 @@ const Editor = (props: EditorProps): JSX.Element => {
const hasChangedContents = updateEditorValues(); const hasChangedContents = updateEditorValues();
if (hasChangedContents && hasEditableRegion()) { if (hasChangedContents && hasEditableRegion()) {
initializeProjectStepFeatures(); initializeDescriptionAndOutputWidgets();
updateDescriptionZone(); updateDescriptionZone();
updateOutputZone(); updateOutputZone();
} }
if (hasChangedContents && !hasEditableRegion()) editor?.focus(); if (hasChangedContents && !hasEditableRegion()) editor?.focus();
if (props.initialTests) initTests(props.initialTests);
if (hasEditableRegion() && editor) { if (hasEditableRegion() && editor) {
if (hasChangedContents) { if (hasChangedContents) {
editor.focus(); editor.focus();
@ -1006,14 +1039,12 @@ const Editor = (props: EditorProps): JSX.Element => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.challengeFiles]); }, [props.challengeFiles]);
useEffect(() => { useEffect(() => {
const { output, tests } = props; const { output } = props;
const editableRegion = getEditableRegionFromRedux(); const editableRegion = getEditableRegionFromRedux();
if (editableRegion.length === 2) { if (editableRegion.length === 2) {
const challengeComplete = tests.every(test => test.pass && !test.err);
const chellengeHasErrors = tests.some(test => test.err);
const testOutput = document.getElementById('test-output'); const testOutput = document.getElementById('test-output');
const testStatus = document.getElementById('test-status'); const testStatus = document.getElementById('test-status');
if (challengeComplete) { if (challengeIsComplete()) {
const testButton = document.getElementById('test-button'); const testButton = document.getElementById('test-button');
if (testButton) { if (testButton) {
testButton.innerHTML = testButton.innerHTML =
@ -1038,7 +1069,7 @@ const Editor = (props: EditorProps): JSX.Element => {
testOutput.innerHTML = ''; testOutput.innerHTML = '';
testStatus.innerHTML = '&#9989; Step completed.'; testStatus.innerHTML = '&#9989; Step completed.';
} }
} else if (chellengeHasErrors && testStatus && testOutput) { } else if (challengeHasErrors() && testStatus && testOutput) {
const wordsArray = [ const wordsArray = [
"Not quite. Here's a hint:", "Not quite. Here's a hint:",
'Try again. This might help:', 'Try again. This might help:',

View File

@ -79,7 +79,7 @@ interface ShowClassicProps {
challengeMounted: (arg0: string) => void; challengeMounted: (arg0: string) => void;
createFiles: (arg0: ChallengeFile[]) => void; createFiles: (arg0: ChallengeFile[]) => void;
data: { challengeNode: ChallengeNodeType }; data: { challengeNode: ChallengeNodeType };
executeChallenge: () => void; executeChallenge: (options?: { showCompletionModal: boolean }) => void;
challengeFiles: ChallengeFiles; challengeFiles: ChallengeFiles;
initConsole: (arg0: string) => void; initConsole: (arg0: string) => void;
initTests: (tests: Test[]) => void; initTests: (tests: Test[]) => void;
@ -315,7 +315,15 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
} }
renderEditor() { renderEditor() {
const { challengeFiles } = this.props; const {
challengeFiles,
data: {
challengeNode: {
fields: { tests },
usesMultifileEditor
}
}
} = this.props;
const { description, title } = this.getChallenge(); const { description, title } = this.getChallenge();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return ( return (
@ -326,8 +334,10 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
description={description} description={description}
editorRef={this.editorRef} editorRef={this.editorRef}
hasEditableBoundries={this.hasEditableBoundries()} hasEditableBoundries={this.hasEditableBoundries()}
initialTests={tests}
resizeProps={this.resizeProps} resizeProps={this.resizeProps}
title={title} title={title}
usesMultifileEditor={usesMultifileEditor}
/> />
) )
); );
@ -368,7 +378,8 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
fields: { blockName }, fields: { blockName },
forumTopicId, forumTopicId,
superBlock, superBlock,
title title,
usesMultifileEditor
} = this.getChallenge(); } = this.getChallenge();
const { const {
executeChallenge, executeChallenge,
@ -387,6 +398,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
instructionsPanelRef={this.instructionsPanelRef} instructionsPanelRef={this.instructionsPanelRef}
nextChallengePath={nextChallengePath} nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath} prevChallengePath={prevChallengePath}
usesMultifileEditor={usesMultifileEditor}
> >
<LearnLayout> <LearnLayout>
<Helmet <Helmet
@ -474,6 +486,7 @@ export const query = graphql`
link link
src src
} }
usesMultifileEditor
challengeFiles { challengeFiles {
fileKey fileKey
ext ext

View File

@ -5,18 +5,29 @@ import React from 'react';
import { HotKeys, GlobalHotKeys } from 'react-hotkeys'; import { HotKeys, GlobalHotKeys } from 'react-hotkeys';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { ChallengeFiles, Test } from '../../../redux/prop-types';
import { canFocusEditorSelector, setEditorFocusability } from '../redux'; import {
canFocusEditorSelector,
setEditorFocusability,
challengeFilesSelector,
submitChallenge,
challengeTestsSelector
} from '../redux';
import './hotkeys.css'; import './hotkeys.css';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
canFocusEditorSelector, canFocusEditorSelector,
(canFocusEditor: boolean) => ({ challengeFilesSelector,
canFocusEditor challengeTestsSelector,
(canFocusEditor: boolean, challengeFiles: ChallengeFiles, tests: Test[]) => ({
canFocusEditor,
challengeFiles,
tests
}) })
); );
const mapDispatchToProps = { setEditorFocusability }; const mapDispatchToProps = { setEditorFocusability, submitChallenge };
const keyMap = { const keyMap = {
NAVIGATION_MODE: 'escape', NAVIGATION_MODE: 'escape',
@ -29,15 +40,19 @@ const keyMap = {
interface HotkeysProps { interface HotkeysProps {
canFocusEditor: boolean; canFocusEditor: boolean;
challengeFiles: ChallengeFiles;
children: React.ReactElement; children: React.ReactElement;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
editorRef?: React.Ref<HTMLElement> | any; editorRef?: React.Ref<HTMLElement> | any;
executeChallenge?: () => void; executeChallenge?: (options?: { showCompletionModal: boolean }) => void;
submitChallenge: () => void;
innerRef: React.Ref<HTMLElement> | unknown; innerRef: React.Ref<HTMLElement> | unknown;
instructionsPanelRef?: React.RefObject<HTMLElement>; instructionsPanelRef?: React.RefObject<HTMLElement>;
nextChallengePath: string; nextChallengePath: string;
prevChallengePath: string; prevChallengePath: string;
setEditorFocusability: (arg0: boolean) => void; setEditorFocusability: (arg0: boolean) => void;
tests: Test[];
usesMultifileEditor?: boolean;
} }
function Hotkeys({ function Hotkeys({
@ -49,7 +64,10 @@ function Hotkeys({
innerRef, innerRef,
nextChallengePath, nextChallengePath,
prevChallengePath, prevChallengePath,
setEditorFocusability setEditorFocusability,
submitChallenge,
tests,
usesMultifileEditor
}: HotkeysProps): JSX.Element { }: HotkeysProps): JSX.Element {
const handlers = { const handlers = {
EXECUTE_CHALLENGE: (e: React.KeyboardEvent<HTMLButtonElement>) => { EXECUTE_CHALLENGE: (e: React.KeyboardEvent<HTMLButtonElement>) => {
@ -58,7 +76,20 @@ function Hotkeys({
// TODO: 'enter' on its own also disables HotKeys, but default behaviour // TODO: 'enter' on its own also disables HotKeys, but default behaviour
// should not be prevented in that case. // should not be prevented in that case.
e.preventDefault(); e.preventDefault();
if (executeChallenge) executeChallenge();
if (!executeChallenge) return;
const testsArePassing = tests.every(test => test.pass && !test.err);
if (usesMultifileEditor) {
if (testsArePassing) {
submitChallenge();
} else {
executeChallenge();
}
} else {
executeChallenge({ showCompletionModal: true });
}
}, },
FOCUS_EDITOR: (e: React.KeyboardEvent) => { FOCUS_EDITOR: (e: React.KeyboardEvent) => {
e.preventDefault(); e.preventDefault();

View File

@ -44,7 +44,7 @@ function ToolPanel({
videoUrl videoUrl
}) { }) {
const handleRunTests = () => { const handleRunTests = () => {
executeChallenge(true); executeChallenge({ showCompletionModal: true });
}; };
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (

View File

@ -75,7 +75,7 @@ interface BackEndProps {
challengeMounted: (arg0: string) => void; challengeMounted: (arg0: string) => void;
data: { challengeNode: ChallengeNodeType }; data: { challengeNode: ChallengeNodeType };
description: string; description: string;
executeChallenge: (arg0: boolean) => void; executeChallenge: (options: { showCompletionModal: boolean }) => void;
forumTopicId: number; forumTopicId: number;
id: string; id: string;
initConsole: () => void; initConsole: () => void;
@ -169,11 +169,13 @@ class BackEnd extends Component<BackEndProps> {
} }
handleSubmit({ handleSubmit({
isShouldCompletionModalOpen showCompletionModal
}: { }: {
isShouldCompletionModalOpen: boolean; showCompletionModal: boolean;
}): void { }): void {
this.props.executeChallenge(isShouldCompletionModalOpen); this.props.executeChallenge({
showCompletionModal
});
} }
render() { render() {

View File

@ -120,11 +120,11 @@ class Project extends Component<ProjectProps> {
} }
handleSubmit({ handleSubmit({
isShouldCompletionModalOpen showCompletionModal
}: { }: {
isShouldCompletionModalOpen: boolean; showCompletionModal: boolean;
}): void { }): void {
if (isShouldCompletionModalOpen) { if (showCompletionModal) {
this.props.openCompletionModal(); this.props.openCompletionModal();
} }
} }

View File

@ -11,7 +11,7 @@ import {
import { Form } from '../../../components/formHelpers'; import { Form } from '../../../components/formHelpers';
interface SubmitProps { interface SubmitProps {
isShouldCompletionModalOpen: boolean; showCompletionModal: boolean;
} }
interface FormProps extends WithTranslation { interface FormProps extends WithTranslation {
@ -43,9 +43,9 @@ export class SolutionForm extends Component<FormProps> {
// updates values on store // updates values on store
this.props.updateSolutionForm(validatedValues.values); this.props.updateSolutionForm(validatedValues.values);
if (validatedValues.invalidValues.length === 0) { if (validatedValues.invalidValues.length === 0) {
this.props.onSubmit({ isShouldCompletionModalOpen: true }); this.props.onSubmit({ showCompletionModal: true });
} else { } else {
this.props.onSubmit({ isShouldCompletionModalOpen: false }); this.props.onSubmit({ showCompletionModal: false });
} }
} }
}; };

View File

@ -47,7 +47,7 @@ export function* executeCancellableChallengeSaga(payload) {
if (previewTask) { if (previewTask) {
yield cancel(previewTask); yield cancel(previewTask);
} }
// executeChallenge with payload containing isShouldCompletionModalOpen // executeChallenge with payload containing {showCompletionModal}
const task = yield fork(executeChallengeSaga, payload); const task = yield fork(executeChallengeSaga, payload);
previewTask = yield fork(previewChallengeSaga, { flushLogs: false }); previewTask = yield fork(previewChallengeSaga, { flushLogs: false });
@ -59,9 +59,7 @@ export function* executeCancellablePreviewSaga() {
previewTask = yield fork(previewChallengeSaga); previewTask = yield fork(previewChallengeSaga);
} }
export function* executeChallengeSaga({ export function* executeChallengeSaga({ payload }) {
payload: isShouldCompletionModalOpen
}) {
const isBuildEnabled = yield select(isBuildEnabledSelector); const isBuildEnabled = yield select(isBuildEnabledSelector);
if (!isBuildEnabled) { if (!isBuildEnabled) {
return; return;
@ -99,7 +97,7 @@ export function* executeChallengeSaga({
yield put(updateTests(testResults)); yield put(updateTests(testResults));
const challengeComplete = testResults.every(test => test.pass && !test.err); const challengeComplete = testResults.every(test => test.pass && !test.err);
if (challengeComplete && isShouldCompletionModalOpen) { if (challengeComplete && payload?.showCompletionModal) {
yield put(openModal('completion')); yield put(openModal('completion'));
} }

View File

@ -1,6 +1,7 @@
{ {
"name": "Accessibility Quiz", "name": "Accessibility Quiz",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "accessibility-quiz", "dashedName": "accessibility-quiz",
"order": 42, "order": 42,
"time": "5 hours", "time": "5 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "Basic CSS Cafe Menu", "name": "Basic CSS Cafe Menu",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "basic-css-cafe-menu", "dashedName": "basic-css-cafe-menu",
"order": 10, "order": 10,
"time": "5 hours", "time": "5 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "Basic HTML Cat Photo App", "name": "Basic HTML Cat Photo App",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "basic-html-cat-photo-app", "dashedName": "basic-html-cat-photo-app",
"order": 9, "order": 9,
"time": "5 hours", "time": "5 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "Basic JavaScript RPG Game", "name": "Basic JavaScript RPG Game",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "basic-javascript-rpg-game", "dashedName": "basic-javascript-rpg-game",
"order": 11, "order": 11,
"time": "2 hours", "time": "2 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "CSS Box Model", "name": "CSS Box Model",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "css-box-model", "dashedName": "css-box-model",
"order": 12, "order": 12,
"time": "5 hours", "time": "5 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "CSS Piano", "name": "CSS Piano",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "css-piano", "dashedName": "css-piano",
"order": 13, "order": 13,
"time": "5 hours", "time": "5 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "CSS Picasso Painting", "name": "CSS Picasso Painting",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "css-picasso-painting", "dashedName": "css-picasso-painting",
"order": 11, "order": 11,
"time": "5 hours", "time": "5 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "CSS Variables Skyline", "name": "CSS Variables Skyline",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "css-variables-skyline", "dashedName": "css-variables-skyline",
"order": 8, "order": 8,
"time": "5 hours", "time": "5 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "D3 Dashboard", "name": "D3 Dashboard",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "d3-dashboard", "dashedName": "d3-dashboard",
"order": 4, "order": 4,
"time": "5 hours", "time": "5 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "Functional Programming Spreadsheet", "name": "Functional Programming Spreadsheet",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "functional-programming-spreadsheet", "dashedName": "functional-programming-spreadsheet",
"order": 13, "order": 13,
"time": "2 hours", "time": "2 hours",

View File

@ -1,6 +1,7 @@
{ {
"name": "Intermediate JavaScript Calorie Counter", "name": "Intermediate JavaScript Calorie Counter",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "intermediate-javascript-calorie-counter", "dashedName": "intermediate-javascript-calorie-counter",
"order": 12, "order": 12,
"time": "2 hours", "time": "2 hours",

View File

@ -287,7 +287,8 @@ ${getFullPath('english')}
isPrivate, isPrivate,
required = [], required = [],
template, template,
time time,
usesMultifileEditor
} = meta; } = meta;
challenge.block = dasherize(blockName); challenge.block = dasherize(blockName);
challenge.order = order; challenge.order = order;
@ -302,6 +303,7 @@ ${getFullPath('english')}
challenge.helpCategory || helpCategoryMap[challenge.block]; challenge.helpCategory || helpCategoryMap[challenge.block];
challenge.translationPending = challenge.translationPending =
lang !== 'english' && !isAuditedCert(lang, superBlock); lang !== 'english' && !isAuditedCert(lang, superBlock);
challenge.usesMultifileEditor = !!usesMultifileEditor;
return prepareChallenge(challenge); return prepareChallenge(challenge);
} }

View File

@ -111,7 +111,8 @@ const schema = Joi.object()
url: Joi.when('challengeType', { url: Joi.when('challengeType', {
is: challengeTypes.codeally, is: challengeTypes.codeally,
then: Joi.string().required() then: Joi.string().required()
}) }),
usesMultifileEditor: Joi.boolean()
}) })
.xor('helpCategory', 'isPrivate'); .xor('helpCategory', 'isPrivate');

View File

@ -1,6 +1,7 @@
{ {
"name": "", "name": "",
"isUpcomingChange": true, "isUpcomingChange": true,
"usesMultifileEditor": true,
"dashedName": "", "dashedName": "",
"order": 42, "order": 42,
"time": "5 hours", "time": "5 hours",