diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index bb452ccbd9..f7ad3aee6c 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -223,6 +223,7 @@ export type ChallengeNodeType = { title: string; translationPending: boolean; url: string; + usesMultifileEditor: boolean; videoId: string; videoLocaleIds?: VideoLocaleIds; bilibiliIds?: BilibiliIds; @@ -436,6 +437,7 @@ export type ChallengeFile = { ext: ExtTypes; name: string; editableRegionBoundaries: number[]; + usesMultifileEditor: boolean; path: string; error: null | string; head: string; diff --git a/client/src/templates/Challenges/classic/MultifileEditor.js b/client/src/templates/Challenges/classic/MultifileEditor.js index b1390e80ea..f0fc86e623 100644 --- a/client/src/templates/Challenges/classic/MultifileEditor.js +++ b/client/src/templates/Challenges/classic/MultifileEditor.js @@ -31,6 +31,7 @@ const propTypes = { fileKey: PropTypes.string, initialEditorContent: PropTypes.string, initialExt: PropTypes.string, + initialTests: PropTypes.array, output: PropTypes.arrayOf(PropTypes.string), resizeProps: PropTypes.shape({ onStopResize: PropTypes.func, @@ -42,6 +43,7 @@ const propTypes = { // TODO: is this used? title: PropTypes.string, updateFile: PropTypes.func.isRequired, + usesMultifileEditor: PropTypes.bool, visibleEditors: PropTypes.shape({ indexjs: PropTypes.bool, indexjsx: PropTypes.bool, @@ -84,10 +86,12 @@ class MultifileEditor extends Component { containerRef, description, editorRef, + initialTests, theme, resizeProps, title, - visibleEditors: { indexcss, indexhtml, indexjs, indexjsx } + visibleEditors: { indexcss, indexhtml, indexjs, indexjsx }, + usesMultifileEditor } = this.props; const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom'; // 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} editorRef={editorRef} fileKey='indexjsx' + initialTests={initialTests} key='indexjsx' resizeProps={resizeProps} theme={editorTheme} title={title} + usesMultifileEditor={usesMultifileEditor} /> )} @@ -159,10 +165,12 @@ class MultifileEditor extends Component { } editorRef={editorRef} fileKey='indexhtml' + initialTests={initialTests} key='indexhtml' resizeProps={resizeProps} theme={editorTheme} title={title} + usesMultifileEditor={usesMultifileEditor} /> )} @@ -177,10 +185,12 @@ class MultifileEditor extends Component { description={targetEditor === 'indexcss' ? description : null} editorRef={editorRef} fileKey='indexcss' + initialTests={initialTests} key='indexcss' resizeProps={resizeProps} theme={editorTheme} title={title} + usesMultifileEditor={usesMultifileEditor} /> )} @@ -196,10 +206,12 @@ class MultifileEditor extends Component { description={targetEditor === 'indexjs' ? description : null} editorRef={editorRef} fileKey='indexjs' + initialTests={initialTests} key='indexjs' resizeProps={resizeProps} theme={editorTheme} title={title} + usesMultifileEditor={usesMultifileEditor} /> )} diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index 8018439524..78bbf70d27 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -37,7 +37,8 @@ import { setEditorFocusability, updateFile, challengeTestsSelector, - submitChallenge + submitChallenge, + initTests } from '../redux'; import './editor.css'; @@ -52,11 +53,14 @@ interface EditorProps { description: string; dimensions: DimensionsType; editorRef: MutableRefObject; - executeChallenge: (isShouldCompletionModalOpen?: boolean) => void; + executeChallenge: (options?: { showCompletionModal: boolean }) => void; ext: ExtTypes; fileKey: FileKeyTypes; initialEditorContent: string; initialExt: string; + initTests: (tests: Test[]) => void; + initialTests: Test[]; + isProjectStep: boolean; output: string[]; resizeProps: ResizePropsType; saveEditorContent: () => void; @@ -70,6 +74,7 @@ interface EditorProps { editorValue: string; editableRegionBoundaries: number[] | null; }) => void; + usesMultifileEditor: boolean; } interface EditorProperties { @@ -122,7 +127,8 @@ const mapDispatchToProps = { saveEditorContent, setEditorFocusability, updateFile, - submitChallenge + submitChallenge, + initTests }; const modeMap = { @@ -181,7 +187,7 @@ const initialData: EditorProperties = { }; 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 // callbacks. Since they have to be initialised before editorWillMount and // 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]; + // 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([]); + testRef.current = props.tests; // 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 @@ -294,7 +305,7 @@ const Editor = (props: EditorProps): JSX.Element => { data.editor = editor; if (hasEditableRegion()) { - initializeProjectStepFeatures(); + initializeDescriptionAndOutputWidgets(); addContentChangeListener(); showEditableRegion(editor); } @@ -342,8 +353,17 @@ const Editor = (props: EditorProps): JSX.Element => { label: 'Run tests', /* eslint-disable no-bitwise */ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], - // TODO: Discuss with Ahmad what should pop-up when a challenge is completed - run: () => props.executeChallenge(true) + run: () => { + if (props.usesMultifileEditor) { + if (challengeIsComplete()) { + props.submitChallenge(); + } else { + props.executeChallenge(); + } + } else { + props.executeChallenge({ showCompletionModal: true }); + } + } }); editor.addAction({ id: 'leave-editor', @@ -648,7 +668,7 @@ const Editor = (props: EditorProps): JSX.Element => { } }; - function initializeProjectStepFeatures() { + function initializeDescriptionAndOutputWidgets() { const editor = data.editor; if (editor) { initializeRegions(getEditableRegionFromRedux()); @@ -955,6 +975,17 @@ const Editor = (props: EditorProps): JSX.Element => { .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(() => { // If a challenge is reset, it needs to communicate that change to the // editor. @@ -962,13 +993,15 @@ const Editor = (props: EditorProps): JSX.Element => { const hasChangedContents = updateEditorValues(); if (hasChangedContents && hasEditableRegion()) { - initializeProjectStepFeatures(); + initializeDescriptionAndOutputWidgets(); updateDescriptionZone(); updateOutputZone(); } if (hasChangedContents && !hasEditableRegion()) editor?.focus(); + if (props.initialTests) initTests(props.initialTests); + if (hasEditableRegion() && editor) { if (hasChangedContents) { editor.focus(); @@ -1006,14 +1039,12 @@ const Editor = (props: EditorProps): JSX.Element => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.challengeFiles]); useEffect(() => { - const { output, tests } = props; + const { output } = props; const editableRegion = getEditableRegionFromRedux(); 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 testStatus = document.getElementById('test-status'); - if (challengeComplete) { + if (challengeIsComplete()) { const testButton = document.getElementById('test-button'); if (testButton) { testButton.innerHTML = @@ -1038,7 +1069,7 @@ const Editor = (props: EditorProps): JSX.Element => { testOutput.innerHTML = ''; testStatus.innerHTML = '✅ Step completed.'; } - } else if (chellengeHasErrors && testStatus && testOutput) { + } else if (challengeHasErrors() && testStatus && testOutput) { const wordsArray = [ "Not quite. Here's a hint:", 'Try again. This might help:', diff --git a/client/src/templates/Challenges/classic/show.tsx b/client/src/templates/Challenges/classic/show.tsx index fc3e217de1..939c415ba4 100644 --- a/client/src/templates/Challenges/classic/show.tsx +++ b/client/src/templates/Challenges/classic/show.tsx @@ -79,7 +79,7 @@ interface ShowClassicProps { challengeMounted: (arg0: string) => void; createFiles: (arg0: ChallengeFile[]) => void; data: { challengeNode: ChallengeNodeType }; - executeChallenge: () => void; + executeChallenge: (options?: { showCompletionModal: boolean }) => void; challengeFiles: ChallengeFiles; initConsole: (arg0: string) => void; initTests: (tests: Test[]) => void; @@ -315,7 +315,15 @@ class ShowClassic extends Component { } renderEditor() { - const { challengeFiles } = this.props; + const { + challengeFiles, + data: { + challengeNode: { + fields: { tests }, + usesMultifileEditor + } + } + } = this.props; const { description, title } = this.getChallenge(); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ( @@ -326,8 +334,10 @@ class ShowClassic extends Component { description={description} editorRef={this.editorRef} hasEditableBoundries={this.hasEditableBoundries()} + initialTests={tests} resizeProps={this.resizeProps} title={title} + usesMultifileEditor={usesMultifileEditor} /> ) ); @@ -368,7 +378,8 @@ class ShowClassic extends Component { fields: { blockName }, forumTopicId, superBlock, - title + title, + usesMultifileEditor } = this.getChallenge(); const { executeChallenge, @@ -387,6 +398,7 @@ class ShowClassic extends Component { instructionsPanelRef={this.instructionsPanelRef} nextChallengePath={nextChallengePath} prevChallengePath={prevChallengePath} + usesMultifileEditor={usesMultifileEditor} > ({ - canFocusEditor + challengeFilesSelector, + challengeTestsSelector, + (canFocusEditor: boolean, challengeFiles: ChallengeFiles, tests: Test[]) => ({ + canFocusEditor, + challengeFiles, + tests }) ); -const mapDispatchToProps = { setEditorFocusability }; +const mapDispatchToProps = { setEditorFocusability, submitChallenge }; const keyMap = { NAVIGATION_MODE: 'escape', @@ -29,15 +40,19 @@ const keyMap = { interface HotkeysProps { canFocusEditor: boolean; + challengeFiles: ChallengeFiles; children: React.ReactElement; // eslint-disable-next-line @typescript-eslint/no-explicit-any editorRef?: React.Ref | any; - executeChallenge?: () => void; + executeChallenge?: (options?: { showCompletionModal: boolean }) => void; + submitChallenge: () => void; innerRef: React.Ref | unknown; instructionsPanelRef?: React.RefObject; nextChallengePath: string; prevChallengePath: string; setEditorFocusability: (arg0: boolean) => void; + tests: Test[]; + usesMultifileEditor?: boolean; } function Hotkeys({ @@ -49,7 +64,10 @@ function Hotkeys({ innerRef, nextChallengePath, prevChallengePath, - setEditorFocusability + setEditorFocusability, + submitChallenge, + tests, + usesMultifileEditor }: HotkeysProps): JSX.Element { const handlers = { EXECUTE_CHALLENGE: (e: React.KeyboardEvent) => { @@ -58,7 +76,20 @@ function Hotkeys({ // TODO: 'enter' on its own also disables HotKeys, but default behaviour // should not be prevented in that case. 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) => { e.preventDefault(); diff --git a/client/src/templates/Challenges/components/Tool-Panel.js b/client/src/templates/Challenges/components/Tool-Panel.js index e0cea1cc23..b6c2aa0a46 100644 --- a/client/src/templates/Challenges/components/Tool-Panel.js +++ b/client/src/templates/Challenges/components/Tool-Panel.js @@ -44,7 +44,7 @@ function ToolPanel({ videoUrl }) { const handleRunTests = () => { - executeChallenge(true); + executeChallenge({ showCompletionModal: true }); }; const { t } = useTranslation(); return ( diff --git a/client/src/templates/Challenges/projects/backend/Show.tsx b/client/src/templates/Challenges/projects/backend/Show.tsx index 468e594ac0..f3a6e990b4 100644 --- a/client/src/templates/Challenges/projects/backend/Show.tsx +++ b/client/src/templates/Challenges/projects/backend/Show.tsx @@ -75,7 +75,7 @@ interface BackEndProps { challengeMounted: (arg0: string) => void; data: { challengeNode: ChallengeNodeType }; description: string; - executeChallenge: (arg0: boolean) => void; + executeChallenge: (options: { showCompletionModal: boolean }) => void; forumTopicId: number; id: string; initConsole: () => void; @@ -169,11 +169,13 @@ class BackEnd extends Component { } handleSubmit({ - isShouldCompletionModalOpen + showCompletionModal }: { - isShouldCompletionModalOpen: boolean; + showCompletionModal: boolean; }): void { - this.props.executeChallenge(isShouldCompletionModalOpen); + this.props.executeChallenge({ + showCompletionModal + }); } render() { diff --git a/client/src/templates/Challenges/projects/frontend/Show.tsx b/client/src/templates/Challenges/projects/frontend/Show.tsx index 0ab1793b7e..71ea456c91 100644 --- a/client/src/templates/Challenges/projects/frontend/Show.tsx +++ b/client/src/templates/Challenges/projects/frontend/Show.tsx @@ -120,11 +120,11 @@ class Project extends Component { } handleSubmit({ - isShouldCompletionModalOpen + showCompletionModal }: { - isShouldCompletionModalOpen: boolean; + showCompletionModal: boolean; }): void { - if (isShouldCompletionModalOpen) { + if (showCompletionModal) { this.props.openCompletionModal(); } } diff --git a/client/src/templates/Challenges/projects/solution-form.tsx b/client/src/templates/Challenges/projects/solution-form.tsx index f802e99b88..95e14a80c3 100644 --- a/client/src/templates/Challenges/projects/solution-form.tsx +++ b/client/src/templates/Challenges/projects/solution-form.tsx @@ -11,7 +11,7 @@ import { import { Form } from '../../../components/formHelpers'; interface SubmitProps { - isShouldCompletionModalOpen: boolean; + showCompletionModal: boolean; } interface FormProps extends WithTranslation { @@ -43,9 +43,9 @@ export class SolutionForm extends Component { // updates values on store this.props.updateSolutionForm(validatedValues.values); if (validatedValues.invalidValues.length === 0) { - this.props.onSubmit({ isShouldCompletionModalOpen: true }); + this.props.onSubmit({ showCompletionModal: true }); } else { - this.props.onSubmit({ isShouldCompletionModalOpen: false }); + this.props.onSubmit({ showCompletionModal: false }); } } }; diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index a9aae70d5a..6b2e1c1108 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -47,7 +47,7 @@ export function* executeCancellableChallengeSaga(payload) { if (previewTask) { yield cancel(previewTask); } - // executeChallenge with payload containing isShouldCompletionModalOpen + // executeChallenge with payload containing {showCompletionModal} const task = yield fork(executeChallengeSaga, payload); previewTask = yield fork(previewChallengeSaga, { flushLogs: false }); @@ -59,9 +59,7 @@ export function* executeCancellablePreviewSaga() { previewTask = yield fork(previewChallengeSaga); } -export function* executeChallengeSaga({ - payload: isShouldCompletionModalOpen -}) { +export function* executeChallengeSaga({ payload }) { const isBuildEnabled = yield select(isBuildEnabledSelector); if (!isBuildEnabled) { return; @@ -99,7 +97,7 @@ export function* executeChallengeSaga({ yield put(updateTests(testResults)); const challengeComplete = testResults.every(test => test.pass && !test.err); - if (challengeComplete && isShouldCompletionModalOpen) { + if (challengeComplete && payload?.showCompletionModal) { yield put(openModal('completion')); } diff --git a/curriculum/challenges/_meta/accessibility-quiz/meta.json b/curriculum/challenges/_meta/accessibility-quiz/meta.json index 4f443ae371..f429ca6ddc 100644 --- a/curriculum/challenges/_meta/accessibility-quiz/meta.json +++ b/curriculum/challenges/_meta/accessibility-quiz/meta.json @@ -1,6 +1,7 @@ { "name": "Accessibility Quiz", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "accessibility-quiz", "order": 42, "time": "5 hours", @@ -279,4 +280,4 @@ "Part 67" ] ] -} \ No newline at end of file +} diff --git a/curriculum/challenges/_meta/basic-css-cafe-menu/meta.json b/curriculum/challenges/_meta/basic-css-cafe-menu/meta.json index e2e4965cf9..fefbd2d7ef 100644 --- a/curriculum/challenges/_meta/basic-css-cafe-menu/meta.json +++ b/curriculum/challenges/_meta/basic-css-cafe-menu/meta.json @@ -1,6 +1,7 @@ { "name": "Basic CSS Cafe Menu", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "basic-css-cafe-menu", "order": 10, "time": "5 hours", diff --git a/curriculum/challenges/_meta/basic-html-cat-photo-app/meta.json b/curriculum/challenges/_meta/basic-html-cat-photo-app/meta.json index 67eafb77e0..c6d611f2a5 100644 --- a/curriculum/challenges/_meta/basic-html-cat-photo-app/meta.json +++ b/curriculum/challenges/_meta/basic-html-cat-photo-app/meta.json @@ -1,6 +1,7 @@ { "name": "Basic HTML Cat Photo App", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "basic-html-cat-photo-app", "order": 9, "time": "5 hours", diff --git a/curriculum/challenges/_meta/basic-javascript-rpg-game/meta.json b/curriculum/challenges/_meta/basic-javascript-rpg-game/meta.json index 6545e469e4..066b4e342c 100644 --- a/curriculum/challenges/_meta/basic-javascript-rpg-game/meta.json +++ b/curriculum/challenges/_meta/basic-javascript-rpg-game/meta.json @@ -1,6 +1,7 @@ { "name": "Basic JavaScript RPG Game", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "basic-javascript-rpg-game", "order": 11, "time": "2 hours", diff --git a/curriculum/challenges/_meta/css-box-model/meta.json b/curriculum/challenges/_meta/css-box-model/meta.json index 8601b698ad..e9b2389f25 100644 --- a/curriculum/challenges/_meta/css-box-model/meta.json +++ b/curriculum/challenges/_meta/css-box-model/meta.json @@ -1,6 +1,7 @@ { "name": "CSS Box Model", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "css-box-model", "order": 12, "time": "5 hours", diff --git a/curriculum/challenges/_meta/css-piano/meta.json b/curriculum/challenges/_meta/css-piano/meta.json index 3b6fb48ca0..15736e4517 100644 --- a/curriculum/challenges/_meta/css-piano/meta.json +++ b/curriculum/challenges/_meta/css-piano/meta.json @@ -1,6 +1,7 @@ { "name": "CSS Piano", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "css-piano", "order": 13, "time": "5 hours", diff --git a/curriculum/challenges/_meta/css-picasso-painting/meta.json b/curriculum/challenges/_meta/css-picasso-painting/meta.json index a906c56c66..521edf4526 100644 --- a/curriculum/challenges/_meta/css-picasso-painting/meta.json +++ b/curriculum/challenges/_meta/css-picasso-painting/meta.json @@ -1,6 +1,7 @@ { "name": "CSS Picasso Painting", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "css-picasso-painting", "order": 11, "time": "5 hours", @@ -367,4 +368,4 @@ "Part 89" ] ] -} \ No newline at end of file +} diff --git a/curriculum/challenges/_meta/css-variables-skyline/meta.json b/curriculum/challenges/_meta/css-variables-skyline/meta.json index b6d37b565d..cbf026ff2d 100644 --- a/curriculum/challenges/_meta/css-variables-skyline/meta.json +++ b/curriculum/challenges/_meta/css-variables-skyline/meta.json @@ -1,6 +1,7 @@ { "name": "CSS Variables Skyline", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "css-variables-skyline", "order": 8, "time": "5 hours", diff --git a/curriculum/challenges/_meta/d3-dashboard/meta.json b/curriculum/challenges/_meta/d3-dashboard/meta.json index 556df4bbee..9e5985d86c 100644 --- a/curriculum/challenges/_meta/d3-dashboard/meta.json +++ b/curriculum/challenges/_meta/d3-dashboard/meta.json @@ -1,6 +1,7 @@ { "name": "D3 Dashboard", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "d3-dashboard", "order": 4, "time": "5 hours", diff --git a/curriculum/challenges/_meta/functional-programming-spreadsheet/meta.json b/curriculum/challenges/_meta/functional-programming-spreadsheet/meta.json index 43ee9f55ec..e2447a7754 100644 --- a/curriculum/challenges/_meta/functional-programming-spreadsheet/meta.json +++ b/curriculum/challenges/_meta/functional-programming-spreadsheet/meta.json @@ -1,6 +1,7 @@ { "name": "Functional Programming Spreadsheet", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "functional-programming-spreadsheet", "order": 13, "time": "2 hours", diff --git a/curriculum/challenges/_meta/intermediate-javascript-calorie-counter/meta.json b/curriculum/challenges/_meta/intermediate-javascript-calorie-counter/meta.json index 6c53ae9841..60827f1a8c 100644 --- a/curriculum/challenges/_meta/intermediate-javascript-calorie-counter/meta.json +++ b/curriculum/challenges/_meta/intermediate-javascript-calorie-counter/meta.json @@ -1,6 +1,7 @@ { "name": "Intermediate JavaScript Calorie Counter", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "intermediate-javascript-calorie-counter", "order": 12, "time": "2 hours", diff --git a/curriculum/getChallenges.js b/curriculum/getChallenges.js index 7bced1789d..bc8a48da37 100644 --- a/curriculum/getChallenges.js +++ b/curriculum/getChallenges.js @@ -287,7 +287,8 @@ ${getFullPath('english')} isPrivate, required = [], template, - time + time, + usesMultifileEditor } = meta; challenge.block = dasherize(blockName); challenge.order = order; @@ -302,6 +303,7 @@ ${getFullPath('english')} challenge.helpCategory || helpCategoryMap[challenge.block]; challenge.translationPending = lang !== 'english' && !isAuditedCert(lang, superBlock); + challenge.usesMultifileEditor = !!usesMultifileEditor; return prepareChallenge(challenge); } diff --git a/curriculum/schema/challengeSchema.js b/curriculum/schema/challengeSchema.js index ff9d2de869..de8016ea63 100644 --- a/curriculum/schema/challengeSchema.js +++ b/curriculum/schema/challengeSchema.js @@ -111,7 +111,8 @@ const schema = Joi.object() url: Joi.when('challengeType', { is: challengeTypes.codeally, then: Joi.string().required() - }) + }), + usesMultifileEditor: Joi.boolean() }) .xor('helpCategory', 'isPrivate'); diff --git a/tools/challenge-helper-scripts/base-meta.json b/tools/challenge-helper-scripts/base-meta.json index a0775e1618..281a91cbce 100644 --- a/tools/challenge-helper-scripts/base-meta.json +++ b/tools/challenge-helper-scripts/base-meta.json @@ -1,6 +1,7 @@ { "name": "", "isUpcomingChange": true, + "usesMultifileEditor": true, "dashedName": "", "order": 42, "time": "5 hours",