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;
|
||||
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;
|
||||
|
@ -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}
|
||||
/>
|
||||
</ReflexElement>
|
||||
)}
|
||||
@ -159,10 +165,12 @@ class MultifileEditor extends Component {
|
||||
}
|
||||
editorRef={editorRef}
|
||||
fileKey='indexhtml'
|
||||
initialTests={initialTests}
|
||||
key='indexhtml'
|
||||
resizeProps={resizeProps}
|
||||
theme={editorTheme}
|
||||
title={title}
|
||||
usesMultifileEditor={usesMultifileEditor}
|
||||
/>
|
||||
</ReflexElement>
|
||||
)}
|
||||
@ -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}
|
||||
/>
|
||||
</ReflexElement>
|
||||
)}
|
||||
@ -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}
|
||||
/>
|
||||
</ReflexElement>
|
||||
)}
|
||||
|
@ -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<editor.IStandaloneCodeEditor>;
|
||||
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<Test[]>([]);
|
||||
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:',
|
||||
|
@ -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<ShowClassicProps, ShowClassicState> {
|
||||
}
|
||||
|
||||
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<ShowClassicProps, ShowClassicState> {
|
||||
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<ShowClassicProps, ShowClassicState> {
|
||||
fields: { blockName },
|
||||
forumTopicId,
|
||||
superBlock,
|
||||
title
|
||||
title,
|
||||
usesMultifileEditor
|
||||
} = this.getChallenge();
|
||||
const {
|
||||
executeChallenge,
|
||||
@ -387,6 +398,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
instructionsPanelRef={this.instructionsPanelRef}
|
||||
nextChallengePath={nextChallengePath}
|
||||
prevChallengePath={prevChallengePath}
|
||||
usesMultifileEditor={usesMultifileEditor}
|
||||
>
|
||||
<LearnLayout>
|
||||
<Helmet
|
||||
@ -474,6 +486,7 @@ export const query = graphql`
|
||||
link
|
||||
src
|
||||
}
|
||||
usesMultifileEditor
|
||||
challengeFiles {
|
||||
fileKey
|
||||
ext
|
||||
|
@ -5,18 +5,29 @@ import React from 'react';
|
||||
import { HotKeys, GlobalHotKeys } from 'react-hotkeys';
|
||||
import { connect } from 'react-redux';
|
||||
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';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
canFocusEditorSelector,
|
||||
(canFocusEditor: boolean) => ({
|
||||
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<HTMLElement> | any;
|
||||
executeChallenge?: () => void;
|
||||
executeChallenge?: (options?: { showCompletionModal: boolean }) => void;
|
||||
submitChallenge: () => void;
|
||||
innerRef: React.Ref<HTMLElement> | unknown;
|
||||
instructionsPanelRef?: React.RefObject<HTMLElement>;
|
||||
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<HTMLButtonElement>) => {
|
||||
@ -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();
|
||||
|
@ -44,7 +44,7 @@ function ToolPanel({
|
||||
videoUrl
|
||||
}) {
|
||||
const handleRunTests = () => {
|
||||
executeChallenge(true);
|
||||
executeChallenge({ showCompletionModal: true });
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
|
@ -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<BackEndProps> {
|
||||
}
|
||||
|
||||
handleSubmit({
|
||||
isShouldCompletionModalOpen
|
||||
showCompletionModal
|
||||
}: {
|
||||
isShouldCompletionModalOpen: boolean;
|
||||
showCompletionModal: boolean;
|
||||
}): void {
|
||||
this.props.executeChallenge(isShouldCompletionModalOpen);
|
||||
this.props.executeChallenge({
|
||||
showCompletionModal
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -120,11 +120,11 @@ class Project extends Component<ProjectProps> {
|
||||
}
|
||||
|
||||
handleSubmit({
|
||||
isShouldCompletionModalOpen
|
||||
showCompletionModal
|
||||
}: {
|
||||
isShouldCompletionModalOpen: boolean;
|
||||
showCompletionModal: boolean;
|
||||
}): void {
|
||||
if (isShouldCompletionModalOpen) {
|
||||
if (showCompletionModal) {
|
||||
this.props.openCompletionModal();
|
||||
}
|
||||
}
|
||||
|
@ -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<FormProps> {
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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'));
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "Accessibility Quiz",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "accessibility-quiz",
|
||||
"order": 42,
|
||||
"time": "5 hours",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "Basic CSS Cafe Menu",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "basic-css-cafe-menu",
|
||||
"order": 10,
|
||||
"time": "5 hours",
|
||||
|
@ -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",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "Basic JavaScript RPG Game",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "basic-javascript-rpg-game",
|
||||
"order": 11,
|
||||
"time": "2 hours",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "CSS Box Model",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "css-box-model",
|
||||
"order": 12,
|
||||
"time": "5 hours",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "CSS Piano",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "css-piano",
|
||||
"order": 13,
|
||||
"time": "5 hours",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "CSS Picasso Painting",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "css-picasso-painting",
|
||||
"order": 11,
|
||||
"time": "5 hours",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "CSS Variables Skyline",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "css-variables-skyline",
|
||||
"order": 8,
|
||||
"time": "5 hours",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "D3 Dashboard",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "d3-dashboard",
|
||||
"order": 4,
|
||||
"time": "5 hours",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "Functional Programming Spreadsheet",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "functional-programming-spreadsheet",
|
||||
"order": 13,
|
||||
"time": "2 hours",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "Intermediate JavaScript Calorie Counter",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "intermediate-javascript-calorie-counter",
|
||||
"order": 12,
|
||||
"time": "2 hours",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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');
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "",
|
||||
"isUpcomingChange": true,
|
||||
"usesMultifileEditor": true,
|
||||
"dashedName": "",
|
||||
"order": 42,
|
||||
"time": "5 hours",
|
||||
|
Reference in New Issue
Block a user