feat: enable reset in multifile editor (#43617)

* feat: dispatch resetChallenge action

* fix: copy challengeFiles instead of in-place sort

* fix: handle null updateFile payloads in redux

* refactor: reorganise region initialization

* refactor: pull code into editorDidMount

Then we can rely on the presence of the editor and monaco and don't have
litter the code with null checks.

* refactor: use better name for editable region init

* refactor: remove unused decoration

* refactor: rename forbidden region init functions

* fix: keep all challengeFiles when resetting

* refactor: remove unused decoration class

* fix: reinitialize editor on reset

* fix: stop adding multiple scroll listeners

Since the challengeFile update on each keystroke extra (unnecessary)
adding of listeners slowed the editor to a crawl.

* fix: only scroll to editor on mount

Rather than on any edit.

* refactor: remove logs and comments

* fix: rename toSortedArray and fix broken test

* fix: check for null not falsy in updateFile

* fix: only update project features when project

* fix: only reset if editor contents have changed

* feat: focus on editor after reset
This commit is contained in:
Oliver Eyton-Williams
2021-10-01 10:36:20 +02:00
committed by GitHub
parent eb6d3e214f
commit e4ba0e23ea
9 changed files with 218 additions and 175 deletions

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
import envData from '../../../../../config/env.json'; import envData from '../../../../../config/env.json';
import { toSortedArray } from '../../../../../utils/sort-files'; import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles';
import EditorTabs from './EditorTabs'; import EditorTabs from './EditorTabs';
import ActionRow from './action-row.tsx'; import ActionRow from './action-row.tsx';
@ -57,7 +57,7 @@ class DesktopLayout extends Component {
getChallengeFile() { getChallengeFile() {
const { challengeFiles } = this.props; const { challengeFiles } = this.props;
return first(toSortedArray(challengeFiles)); return first(sortChallengeFiles(challengeFiles));
} }
render() { render() {

View File

@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { toSortedArray } from '../../../../../utils/sort-files'; import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles';
import { import {
toggleVisibleEditor, toggleVisibleEditor,
visibleEditorsSelector, visibleEditorsSelector,
@ -39,7 +39,7 @@ class EditorTabs extends Component {
const { challengeFiles, toggleVisibleEditor, visibleEditors } = this.props; const { challengeFiles, toggleVisibleEditor, visibleEditors } = this.props;
return ( return (
<div className='monaco-editor-tabs'> <div className='monaco-editor-tabs'>
{toSortedArray(challengeFiles).map(challengeFile => ( {sortChallengeFiles(challengeFiles).map(challengeFile => (
<button <button
aria-selected={visibleEditors[challengeFile.fileKey]} aria-selected={visibleEditors[challengeFile.fileKey]}
className='monaco-editor-tab' className='monaco-editor-tab'

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import BreadCrumb from '../components/bread-crumb'; import BreadCrumb from '../components/bread-crumb';
import { resetChallenge } from '../redux';
import EditorTabs from './EditorTabs'; import EditorTabs from './EditorTabs';
interface ActionRowProps { interface ActionRowProps {
@ -9,18 +11,21 @@ interface ActionRowProps {
showPreview: boolean; showPreview: boolean;
superBlock: string; superBlock: string;
switchDisplayTab: (displayTab: string) => void; switchDisplayTab: (displayTab: string) => void;
resetChallenge: () => void;
} }
const mapDispatchToProps = {
resetChallenge
};
const ActionRow = ({ const ActionRow = ({
switchDisplayTab, switchDisplayTab,
showPreview, showPreview,
showConsole, showConsole,
superBlock, superBlock,
block block,
resetChallenge
}: ActionRowProps): JSX.Element => { }: ActionRowProps): JSX.Element => {
const restartStep = () => {
console.log('restart');
};
return ( return (
<div className='action-row'> <div className='action-row'>
<div className='breadcrumbs-demo'> <div className='breadcrumbs-demo'>
@ -30,7 +35,7 @@ const ActionRow = ({
<EditorTabs /> <EditorTabs />
<button <button
className='restart-step-tab' className='restart-step-tab'
onClick={() => restartStep()} onClick={resetChallenge}
role='tab' role='tab'
> >
Restart Step Restart Step
@ -57,4 +62,5 @@ const ActionRow = ({
}; };
ActionRow.displayName = 'ActionRow'; ActionRow.displayName = 'ActionRow';
export default ActionRow;
export default connect(null, mapDispatchToProps)(ActionRow);

View File

@ -262,9 +262,6 @@ const Editor = (props: EditorProps): JSX.Element => {
modeMap[challengeFile?.ext ?? 'html'] modeMap[challengeFile?.ext ?? 'html']
); );
data.model = model; data.model = model;
const editableRegion = getEditableRegionFromRedux();
if (editableRegion.length === 2) decorateForbiddenRanges(editableRegion);
// TODO: do we need to return this? // TODO: do we need to return this?
return { model }; return { model };
@ -274,13 +271,16 @@ const Editor = (props: EditorProps): JSX.Element => {
// changes coming from outside the editor (such as code resets). // changes coming from outside the editor (such as code resets).
const updateEditorValues = () => { const updateEditorValues = () => {
const { challengeFiles, fileKey } = props; const { challengeFiles, fileKey } = props;
const { model } = dataRef.current[fileKey]; const { model } = data;
const newContents = challengeFiles?.find( const newContents = challengeFiles?.find(
challengeFile => challengeFile.fileKey === fileKey challengeFile => challengeFile.fileKey === fileKey
)?.contents; )?.contents;
if (model?.getValue() !== newContents) { if (model?.getValue() !== newContents) {
model?.setValue(newContents ?? ''); model?.setValue(newContents ?? '');
return true;
} else {
return false;
} }
}; };
@ -292,6 +292,12 @@ const Editor = (props: EditorProps): JSX.Element => {
editorRef.current = editor; editorRef.current = editor;
data.editor = editor; data.editor = editor;
if (isProject()) {
initializeProjectFeatures();
addContentChangeListener();
showEditableRegion(editor);
}
const storedAccessibilityMode = () => { const storedAccessibilityMode = () => {
const accessibility = store.get('accessibilityMode') as boolean; const accessibility = store.get('accessibilityMode') as boolean;
if (!accessibility) { if (!accessibility) {
@ -368,65 +374,6 @@ const Editor = (props: EditorProps): JSX.Element => {
} }
}); });
editor.onDidFocusEditorWidget(() => props.setEditorFocusability(true)); editor.onDidFocusEditorWidget(() => props.setEditorFocusability(true));
const editableBoundaries = getEditableRegionFromRedux();
if (editableBoundaries.length === 2) {
const createWidget = (
id: string,
domNode: HTMLDivElement,
getTop: () => string
) => {
const getId = () => id;
const getDomNode = () => domNode;
const getPosition = () => {
domNode.style.width = `${editor.getLayoutInfo().contentWidth}px`;
domNode.style.top = getTop();
// must return null, so that Monaco knows the widget will position
// itself.
return null;
};
return {
getId,
getDomNode,
getPosition
};
};
const descriptionNode = createDescription(editor);
const outputNode = createOutputNode(editor);
const descriptionWidget = createWidget(
'description.widget',
descriptionNode,
getDescriptionZoneTop
);
data.descriptionWidget = descriptionWidget;
const outputWidget = createWidget(
'output.widget',
outputNode,
getOutputZoneTop
);
data.outputWidget = outputWidget;
editor.addOverlayWidget(descriptionWidget);
// TODO: order of insertion into the DOM probably matters, revisit once
// the tabs have been fixed!
editor.addOverlayWidget(outputWidget);
editor.changeViewZones(descriptionZoneCallback);
editor.changeViewZones(outputZoneCallback);
editor.onDidScrollChange(() => {
editor.layoutOverlayWidget(descriptionWidget);
editor.layoutOverlayWidget(outputWidget);
});
showEditableRegion(editableBoundaries);
}
}; };
const descriptionZoneCallback = ( const descriptionZoneCallback = (
@ -581,28 +528,24 @@ const Editor = (props: EditorProps): JSX.Element => {
updateFile({ fileKey, editorValue, editableRegionBoundaries }); updateFile({ fileKey, editorValue, editableRegionBoundaries });
}; };
function showEditableRegion(editableBoundaries: number[]) { // TODO DRY this and the update function
if (editableBoundaries.length !== 2) return; function initializeForbiddenRegion(
const editor = data.editor; stickiness: number,
if (!editor) return; target: editor.ITextModel,
// TODO: The heuristic has been commented out for now because the cursor range: IRange
// position is not saved at the moment, so it's redundant. I'm leaving it ) {
// here for now, in case we decide to save it in future. const lineDecoration = {
// this is a heuristic: if the cursor is at the start of the page, chances range,
// are the user has not edited yet. If so, move to the start of the editable options: {
// region. isWholeLine: true,
// if ( linesDecorationsClassName: 'myLineDecoration',
// isEqual({ ..._editor.getPosition() }, { lineNumber: 1, column: 1 }) stickiness
// ) { }
editor.setPosition({ };
lineNumber: editableBoundaries[0] + 1, return target.deltaDecorations([], [lineDecoration]);
column: 1
});
editor.revealLinesInCenter(editableBoundaries[0], editableBoundaries[1]);
// }
} }
function highlightLines( function updateForbiddenRegion(
stickiness: number, stickiness: number,
target: editor.ITextModel, target: editor.ITextModel,
range: IRange, range: IRange,
@ -613,14 +556,30 @@ const Editor = (props: EditorProps): JSX.Element => {
options: { options: {
isWholeLine: true, isWholeLine: true,
linesDecorationsClassName: 'myLineDecoration', linesDecorationsClassName: 'myLineDecoration',
className: 'do-not-edit',
stickiness stickiness
} }
}; };
return target.deltaDecorations(oldIds, [lineDecoration]); return target.deltaDecorations(oldIds, [lineDecoration]);
} }
function highlightEditableLines( // TODO: DRY this and the update function
function initializeEditableRegion(
stickiness: number,
target: editor.ITextModel,
range: IRange
) {
const lineDecoration = {
range,
options: {
isWholeLine: true,
linesDecorationsClassName: 'myEditableLineDecoration',
stickiness
}
};
return target.deltaDecorations([], [lineDecoration]);
}
function updateEditableRegion(
stickiness: number, stickiness: number,
target: editor.ITextModel, target: editor.ITextModel,
range: IRange, range: IRange,
@ -631,30 +590,12 @@ const Editor = (props: EditorProps): JSX.Element => {
options: { options: {
isWholeLine: true, isWholeLine: true,
linesDecorationsClassName: 'myEditableLineDecoration', linesDecorationsClassName: 'myEditableLineDecoration',
className: 'do-not-edit',
stickiness stickiness
} }
}; };
return target.deltaDecorations(oldIds, [lineDecoration]); return target.deltaDecorations(oldIds, [lineDecoration]);
} }
function highlightText(
stickiness: number,
target: editor.ITextModel,
range: IRange,
oldIds: string[] = []
) {
const inlineDecoration = {
range,
options: {
inlineClassName: 'myInlineDecoration',
stickiness
}
};
return target.deltaDecorations(oldIds, [inlineDecoration]);
}
function getDescriptionZoneTop() { function getDescriptionZoneTop() {
return `${data.descriptionZoneTop}px`; return `${data.descriptionZoneTop}px`;
} }
@ -705,7 +646,20 @@ const Editor = (props: EditorProps): JSX.Element => {
} }
}; };
function decorateForbiddenRanges(editableRegion: number[]) { function initializeProjectFeatures() {
const editor = data.editor;
if (editor) {
initializeRegions(getEditableRegionFromRedux());
addWidgetsToRegions(editor);
}
}
function isProject() {
const editableRegionBoundaries = getEditableRegionFromRedux();
return editableRegionBoundaries.length === 2;
}
function initializeRegions(editableRegion: number[]) {
const { model } = data; const { model } = data;
const monaco = monacoRef.current; const monaco = monacoRef.current;
if (!model || !monaco) return; if (!model || !monaco) return;
@ -714,12 +668,12 @@ const Editor = (props: EditorProps): JSX.Element => {
[editableRegion[1], model.getLineCount()] [editableRegion[1], model.getLineCount()]
]; ];
const editableRange = positionsToRange(model, monaco, [ const editableRange = positionsToRange(monaco, model, [
editableRegion[0] + 1, editableRegion[0] + 1,
editableRegion[1] - 1 editableRegion[1] - 1
]); ]);
data.insideEditDecId = highlightEditableLines( data.insideEditDecId = initializeEditableRegion(
monaco.editor.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, monaco.editor.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
model, model,
editableRange editableRange
@ -729,51 +683,87 @@ const Editor = (props: EditorProps): JSX.Element => {
// we simply don't add those decorations // we simply don't add those decorations
if (forbiddenRegions[0][1] > 0) { if (forbiddenRegions[0][1] > 0) {
const forbiddenRange = positionsToRange( const forbiddenRange = positionsToRange(
model,
monaco, monaco,
model,
forbiddenRegions[0] forbiddenRegions[0]
); );
// the first range should expand at the top // the first range should expand at the top
// TODO: Unsure what this should be - returns an array, so I added [0] @ojeytonwilliams // TODO: Unsure what this should be - returns an array, so I added [0] @ojeytonwilliams
data.startEditDecId = highlightLines( data.startEditDecId = initializeForbiddenRegion(
monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore, monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore,
model, model,
forbiddenRange forbiddenRange
)[0]; )[0];
highlightText(
monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore,
model,
forbiddenRange
);
} }
const forbiddenRange = positionsToRange(model, monaco, forbiddenRegions[1]); const forbiddenRange = positionsToRange(monaco, model, forbiddenRegions[1]);
// TODO: handle the case the region covers the bottom of the editor // TODO: handle the case the region covers the bottom of the editor
// the second range should expand at the bottom // the second range should expand at the bottom
data.endEditDecId = highlightLines( data.endEditDecId = initializeForbiddenRegion(
monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter, monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter,
model, model,
forbiddenRange forbiddenRange
)[0]; )[0];
}
highlightText( function addWidgetsToRegions(editor: editor.IStandaloneCodeEditor) {
monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter, const createWidget = (
model, id: string,
forbiddenRange domNode: HTMLDivElement,
); getTop: () => string
) => {
const getId = () => id;
const getDomNode = () => domNode;
const getPosition = () => {
domNode.style.width = `${editor.getLayoutInfo().contentWidth}px`;
domNode.style.top = getTop();
// The deleted line is always considered to be the one that has moved up. // must return null, so that Monaco knows the widget will position
// - if the user deletes at the end of line 5, line 6 is deleted and // itself.
// - if the user backspaces at the start of line 6, line 6 is deleted return null;
// TODO: handle multiple simultaneous changes (multicursors do this) };
function getDeletedLine(event: editor.IModelContentChangedEvent) { return {
const isDeleted = getId,
event.changes[0].text === '' && event.changes[0].range.endColumn === 1; getDomNode,
return isDeleted ? event.changes[0].range.endLineNumber : 0; getPosition
};
};
const descriptionNode = createDescription(editor);
const outputNode = createOutputNode(editor);
if (!data.descriptionWidget) {
data.descriptionWidget = createWidget(
'description.widget',
descriptionNode,
getDescriptionZoneTop
);
editor.addOverlayWidget(data.descriptionWidget);
editor.changeViewZones(descriptionZoneCallback);
}
if (!data.outputWidget) {
data.outputWidget = createWidget(
'output.widget',
outputNode,
getOutputZoneTop
);
editor.addOverlayWidget(data.outputWidget);
editor.changeViewZones(outputZoneCallback);
} }
// TODO this listener needs to be replaced on reset. editor.onDidScrollChange(() => {
if (data.descriptionWidget)
editor.layoutOverlayWidget(data.descriptionWidget);
if (data.outputWidget) editor.layoutOverlayWidget(data.outputWidget);
});
}
function addContentChangeListener() {
const { model } = data;
const monaco = monacoRef.current;
if (!model || !monaco) return;
model.onDidChangeContent(e => { model.onDidChangeContent(e => {
// TODO: it would be nice if undoing could remove the warning, but // TODO: it would be nice if undoing could remove the warning, but
// it's probably too hard to track. i.e. if they make two warned edits // it's probably too hard to track. i.e. if they make two warned edits
@ -793,7 +783,7 @@ const Editor = (props: EditorProps): JSX.Element => {
const redecorateEditableRegion = () => { const redecorateEditableRegion = () => {
const coveringRange = getLinesCoveringEditableRegion(); const coveringRange = getLinesCoveringEditableRegion();
if (coveringRange) { if (coveringRange) {
data.insideEditDecId = highlightEditableLines( data.insideEditDecId = updateEditableRegion(
monaco.editor.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, monaco.editor.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
model, model,
coveringRange, coveringRange,
@ -829,7 +819,7 @@ const Editor = (props: EditorProps): JSX.Element => {
const preventOverlap = ( const preventOverlap = (
id: string, id: string,
stickiness: number, stickiness: number,
highlightFunction: typeof highlightLines updateRegion: typeof updateForbiddenRegion
) => { ) => {
// Even though the decoration covers the whole line, it has a // Even though the decoration covers the whole line, it has a
// startColumn that moves. toStartOfLine ensures that the // startColumn that moves. toStartOfLine ensures that the
@ -867,7 +857,7 @@ const Editor = (props: EditorProps): JSX.Element => {
if (touchingDeleted) { if (touchingDeleted) {
// TODO: if they undo this should be reversed // TODO: if they undo this should be reversed
const decorations = highlightFunction( const decorations = updateRegion(
stickiness, stickiness,
model, model,
newCoveringRange, newCoveringRange,
@ -889,7 +879,7 @@ const Editor = (props: EditorProps): JSX.Element => {
data.endEditDecId = preventOverlap( data.endEditDecId = preventOverlap(
data.endEditDecId, data.endEditDecId,
monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore, monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore,
highlightLines updateForbiddenRegion
); );
// If the content has changed, the zones may need moving. Rather than // If the content has changed, the zones may need moving. Rather than
@ -905,13 +895,44 @@ const Editor = (props: EditorProps): JSX.Element => {
warnUser(data.endEditDecId); warnUser(data.endEditDecId);
} }
}); });
// The deleted line is always considered to be the one that has moved up.
// - if the user deletes at the end of line 5, line 6 is deleted and
// - if the user backspaces at the start of line 6, line 6 is deleted
// TODO: handle multiple simultaneous changes (multicursors do this)
function getDeletedLine(event: editor.IModelContentChangedEvent) {
const isDeleted =
event.changes[0].text === '' && event.changes[0].range.endColumn === 1;
return isDeleted ? event.changes[0].range.endLineNumber : 0;
}
}
function showEditableRegion(editor: editor.IStandaloneCodeEditor) {
const editableRegionBoundaries = getEditableRegionFromRedux();
// TODO: The heuristic has been commented out for now because the cursor
// position is not saved at the moment, so it's redundant. I'm leaving it
// here for now, in case we decide to save it in future.
// this is a heuristic: if the cursor is at the start of the page, chances
// are the user has not edited yet. If so, move to the start of the editable
// region.
// if (
// isEqual({ ..._editor.getPosition() }, { lineNumber: 1, column: 1 })
// ) {
editor.setPosition({
lineNumber: editableRegionBoundaries[0] + 1,
column: 1
});
editor.revealLinesInCenter(
editableRegionBoundaries[0],
editableRegionBoundaries[1]
);
// }
} }
// creates a range covering all the lines in 'positions' // creates a range covering all the lines in 'positions'
// NOTE: positions is an array of [startLine, endLine] // NOTE: positions is an array of [startLine, endLine]
function positionsToRange( function positionsToRange(
model: editor.ITextModel,
monaco: typeof monacoEditor, monaco: typeof monacoEditor,
model: editor.ITextModel,
[start, end]: [number, number] [start, end]: [number, number]
) { ) {
// convert to [startLine, startColumn, endLine, endColumn] // convert to [startLine, startColumn, endLine, endColumn]
@ -933,7 +954,19 @@ const Editor = (props: EditorProps): JSX.Element => {
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.
updateEditorValues(); const { editor } = data;
const hasChangedContents = updateEditorValues();
if (hasChangedContents && isProject()) {
initializeProjectFeatures();
updateDescriptionZone();
updateOutputZone();
}
editor?.focus();
if (isProject() && editor) {
showEditableRegion(editor);
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.challengeFiles]); }, [props.challengeFiles]);
useEffect(() => { useEffect(() => {

View File

@ -215,15 +215,24 @@ export const reducer = handleActions(
state, state,
{ payload: { fileKey, editorValue, editableRegionBoundaries } } { payload: { fileKey, editorValue, editableRegionBoundaries } }
) => { ) => {
const updates = {};
// if a given part of the payload is null, we leave that part of the state
// unchanged
if (editableRegionBoundaries !== null)
updates.editableRegionBoundaries = editableRegionBoundaries;
if (editorValue !== null) updates.contents = editorValue;
if (editableRegionBoundaries !== null && editorValue !== null)
updates.editableContents = getLines(
editorValue,
editableRegionBoundaries
);
return { return {
...state, ...state,
challengeFiles: [ challengeFiles: [
...state.challengeFiles.filter(x => x.fileKey !== fileKey), ...state.challengeFiles.filter(x => x.fileKey !== fileKey),
{ {
...state.challengeFiles.find(x => x.fileKey === fileKey), ...state.challengeFiles.find(x => x.fileKey === fileKey),
contents: editorValue, ...updates
editableContents: getLines(editorValue, editableRegionBoundaries),
editableRegionBoundaries
} }
] ]
}; };
@ -268,21 +277,16 @@ export const reducer = handleActions(
challengeMeta: { ...payload } challengeMeta: { ...payload }
}), }),
[actionTypes.resetChallenge]: state => { [actionTypes.resetChallenge]: state => {
const challengeFilesReset = [ const challengeFilesReset = state.challengeFiles.map(challengeFile => ({
...state.challengeFiles.reduce( ...challengeFile,
(challengeFiles, challengeFile) => ({ contents: challengeFile.seed.slice(),
...challengeFiles, editableContents: getLines(
...challengeFile, challengeFile.seed,
contents: challengeFile.seed.slice(), challengeFile.seedEditableRegionBoundaries
editableContents: getLines( ),
challengeFile.seed, editableRegionBoundaries:
challengeFile.seedEditableRegionBoundaries challengeFile.seedEditableRegionBoundaries.slice()
), }));
editableRegionBoundaries: challengeFile.seedEditableRegionBoundaries
}),
{}
)
];
return { return {
...state, ...state,
currentTab: 2, currentTab: 2,

View File

@ -1,5 +1,5 @@
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { toSortedArray } from '../../../../../utils/sort-files'; import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles';
export function getTargetEditor(challengeFiles) { export function getTargetEditor(challengeFiles) {
if (isEmpty(challengeFiles)) return null; if (isEmpty(challengeFiles)) return null;
@ -9,6 +9,6 @@ export function getTargetEditor(challengeFiles) {
)?.fileKey; )?.fileKey;
// fallback for when there is no editable region. // fallback for when there is no editable region.
return targetEditor || toSortedArray(challengeFiles)[0].fileKey; return targetEditor || sortChallengeFiles(challengeFiles)[0].fileKey;
} }
} }

View File

@ -43,7 +43,7 @@ const testEvaluator =
const { getLines } = require('../../utils/get-lines'); const { getLines } = require('../../utils/get-lines');
const { isAuditedCert } = require('../../utils/is-audited'); const { isAuditedCert } = require('../../utils/is-audited');
const { toSortedArray } = require('../../utils/sort-files'); const { sortChallengeFiles } = require('../../utils/sort-challengefiles');
const { const {
getChallengesForLang, getChallengesForLang,
getMetaForBlock, getMetaForBlock,
@ -471,7 +471,7 @@ ${inspect(commentMap)}
// TODO: the no-solution filtering is a little convoluted: // TODO: the no-solution filtering is a little convoluted:
const noSolution = new RegExp('// solution required'); const noSolution = new RegExp('// solution required');
const solutionsAsArrays = solutions.map(toSortedArray); const solutionsAsArrays = solutions.map(sortChallengeFiles);
const filteredSolutions = solutionsAsArrays.filter(solution => { const filteredSolutions = solutionsAsArrays.filter(solution => {
return !isEmpty( return !isEmpty(

View File

@ -1,5 +1,5 @@
exports.toSortedArray = function toSortedArray(challengeFiles) { exports.sortChallengeFiles = function sortChallengeFiles(challengeFiles) {
const xs = challengeFiles; const xs = challengeFiles.slice();
// TODO: refactor this to use an ext array ['html', 'js', 'css'] and loop over // TODO: refactor this to use an ext array ['html', 'js', 'css'] and loop over
// that. // that.
xs.sort((a, b) => { xs.sort((a, b) => {

View File

@ -1,21 +1,21 @@
const { challengeFiles } = require('./__fixtures__/challenges'); const { challengeFiles } = require('./__fixtures__/challenges');
const { toSortedArray } = require('./sort-files'); const { sortChallengeFiles } = require('./sort-challengefiles');
describe('sort-files', () => { describe('sort-files', () => {
describe('toSortedArray', () => { describe('sortChallengeFiles', () => {
it('should return an array', () => { it('should return an array', () => {
const sorted = toSortedArray(challengeFiles); const sorted = sortChallengeFiles(challengeFiles);
expect(Array.isArray(sorted)).toBe(true); expect(Array.isArray(sorted)).toBe(true);
}); });
it('should not modify the challenges', () => { it('should not modify the challenges', () => {
const sorted = toSortedArray(challengeFiles); const sorted = sortChallengeFiles(challengeFiles);
const expected = challengeFiles; const expected = challengeFiles;
expect(sorted).toEqual(expect.arrayContaining(expected)); expect(sorted).toEqual(expect.arrayContaining(expected));
expect(sorted.length).toEqual(expected.length); expect(sorted.length).toEqual(expected.length);
}); });
it('should sort the objects into html, css, jsx, js order', () => { it('should sort the objects into html, css, jsx, js order', () => {
const sorted = challengeFiles; const sorted = sortChallengeFiles(challengeFiles);
const sortedKeys = sorted.map(({ fileKey }) => fileKey); const sortedKeys = sorted.map(({ fileKey }) => fileKey);
const expected = ['indexhtml', 'indexcss', 'indexjsx', 'indexjs']; const expected = ['indexhtml', 'indexcss', 'indexjsx', 'indexjs'];
expect(sortedKeys).toStrictEqual(expected); expect(sortedKeys).toStrictEqual(expected);