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