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:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							9220bfedad
						
					
				
				
					commit
					22afdd1aad
				
			@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 = '✅ Step completed.';
 | 
					          testStatus.innerHTML = '✅ 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:',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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'));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "",
 | 
					  "name": "",
 | 
				
			||||||
  "isUpcomingChange": true,
 | 
					  "isUpcomingChange": true,
 | 
				
			||||||
 | 
					  "usesMultifileEditor": true,
 | 
				
			||||||
  "dashedName": "",
 | 
					  "dashedName": "",
 | 
				
			||||||
  "order": 42,
 | 
					  "order": 42,
 | 
				
			||||||
  "time": "5 hours",
 | 
					  "time": "5 hours",
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user