refactor(client): simplify sort challenge files (#59179)

Co-authored-by: Naomi <accounts+github@nhcarrigan.com>
This commit is contained in:
Oliver Eyton-Williams
2025-04-02 20:50:43 +02:00
committed by GitHub
parent 6e1b87cc78
commit a82316e469
10 changed files with 64 additions and 94 deletions
+2 -9
View File
@@ -198,7 +198,7 @@ export type ChallengeNode = {
required: Required[];
scene: FullScene;
solutions: {
[T in FileKey]: FileKeyChallenge;
[T: string]: FileKeyChallenge;
};
sourceInstanceName: string;
superOrder: number;
@@ -383,13 +383,6 @@ export interface ChallengeData extends CompletedChallenge {
challengeFiles: ChallengeFile[] | null;
}
export type FileKey =
| 'scriptjs'
| 'indexts'
| 'indexhtml'
| 'stylescss'
| 'indexjsx';
export type ChallengeMeta = {
block: string;
id: string;
@@ -421,7 +414,7 @@ export type FileKeyChallenge = {
ext: Ext;
head: string;
id: string;
key: FileKey;
key: string;
name: string;
tail: string;
};
@@ -1,16 +1,11 @@
import { first } from 'lodash-es';
import { isEmpty } from 'lodash-es';
import React, { useState, useEffect, ReactElement } from 'react';
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import store from 'store';
import { sortChallengeFiles } from '../../../../utils/sort-challengefiles';
import { challengeTypes } from '../../../../../shared/config/challenge-types';
import {
ChallengeFile,
ChallengeFiles,
ResizeProps
} from '../../../redux/prop-types';
import { ChallengeFiles, ResizeProps } from '../../../redux/prop-types';
import {
removePortalWindow,
setShowPreviewPortal,
@@ -204,12 +199,8 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
}
};
const getChallengeFile = () => {
const { challengeFiles } = props;
return first(sortChallengeFiles(challengeFiles) as ChallengeFile[]);
};
const {
challengeFiles,
challengeType,
resizeProps,
instructions,
@@ -235,7 +226,6 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
}
}, []);
const challengeFile = getChallengeFile();
const projectBasedChallenge = hasEditableBoundaries;
const isMultifileProject =
challengeType === challengeTypes.multifileCertProject ||
@@ -301,11 +291,8 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
{...resizeProps}
data-playwright-test-label='editor-pane'
>
{challengeFile && (
<ReflexContainer
key={challengeFile.fileKey}
orientation='horizontal'
>
{!isEmpty(challengeFiles) && (
<ReflexContainer key='codePane' orientation='horizontal'>
<ReflexElement
name='codePane'
{...(displayEditorConsole && { flex: codePane.flex })}
@@ -39,35 +39,29 @@ class EditorTabs extends Component<EditorTabsProps> {
render() {
const { challengeFiles, toggleVisibleEditor, visibleEditors } = this.props;
const isMobile = window.innerWidth < MAX_MOBILE_WIDTH;
const isRenderChallengeFiles =
/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */
!isMobile || sortChallengeFiles(challengeFiles).length > 1;
const sortedFiles = sortChallengeFiles(challengeFiles ?? []);
const showTabs = !isMobile || sortedFiles.length > 1;
return (
isRenderChallengeFiles && (
showTabs && (
<div className='monaco-editor-tabs'>
{
/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
sortChallengeFiles(challengeFiles).map(
(challengeFile: ChallengeFile) => (
<button
aria-expanded={
// @ts-expect-error TODO: validate challengeFile on io-boundary,
// then we won't need to ignore this error and we can drop the
// nullish handling.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
visibleEditors[challengeFile.fileKey] ?? 'false'
}
key={challengeFile.fileKey}
onClick={() => toggleVisibleEditor(challengeFile.fileKey)}
>
{`${challengeFile.name}.${challengeFile.ext}`}{' '}
<span className='sr-only'>
{i18next.t('learn.editor-tabs.editor')}
</span>
</button>
)
)
}
{sortedFiles.map((challengeFile: ChallengeFile) => (
<button
aria-expanded={
// @ts-expect-error TODO: validate challengeFile on io-boundary,
// then we won't need to ignore this error and we can drop the
// nullish handling.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
visibleEditors[challengeFile.fileKey] ?? 'false'
}
key={challengeFile.fileKey}
onClick={() => toggleVisibleEditor(challengeFile.fileKey)}
>
{`${challengeFile.name}.${challengeFile.ext}`}{' '}
<span className='sr-only'>
{i18next.t('learn.editor-tabs.editor')}
</span>
</button>
))}
</div>
)
);
@@ -25,10 +25,9 @@ import {
isSignedInSelector,
themeSelector
} from '../../../redux/selectors';
import {
import type {
ChallengeFiles,
Dimensions,
FileKey,
ResizeProps,
Test
} from '../../../redux/prop-types';
@@ -85,7 +84,7 @@ export interface EditorProps {
dimensions?: Dimensions;
editorRef: MutableRefObject<editor.IStandaloneCodeEditor | undefined>;
executeChallenge: (options?: { showCompletionModal: boolean }) => void;
fileKey: FileKey;
fileKey: string;
canFocusOnMountRef: MutableRefObject<boolean>;
initTests: (tests: Test[]) => void;
initialTests: Test[];
@@ -108,7 +107,7 @@ export interface EditorProps {
showProjectPreview: boolean;
previewOpen: boolean;
updateFile: (object: {
fileKey: FileKey;
fileKey: string;
editorValue: string;
editableRegionBoundaries?: number[];
}) => void;
@@ -10,7 +10,6 @@ import {
} from '../redux/selectors';
import { getTargetEditor } from '../utils/get-target-editor';
import './editor.css';
import { FileKey } from '../../../redux/prop-types';
import Editor, { type EditorProps } from './editor';
export type VisibleEditors = {
@@ -146,7 +145,7 @@ const MultifileEditor = (props: MultifileEditorProps) => {
containerRef={containerRef}
description={targetEditor === key ? description : ''}
editorRef={editorRef}
fileKey={key as FileKey}
fileKey={key}
initialTests={initialTests}
isMobileLayout={isMobileLayout}
isUsingKeyboardInTablist={isUsingKeyboardInTablist}
@@ -1,10 +1,8 @@
import { isEmpty } from 'lodash-es';
import { sortChallengeFiles } from '../../../../utils/sort-challengefiles';
import { ChallengeFiles, FileKey } from '../../../redux/prop-types';
import { ChallengeFiles } from '../../../redux/prop-types';
export function getTargetEditor(
challengeFiles: ChallengeFiles
): FileKey | null {
export function getTargetEditor(challengeFiles: ChallengeFiles): string | null {
if (isEmpty(challengeFiles)) return null;
const targetEditor = challengeFiles?.find(
@@ -12,6 +10,6 @@ export function getTargetEditor(
)?.fileKey;
// fallback for when there is no editable region.
/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */
return targetEditor || sortChallengeFiles(challengeFiles)[0].fileKey;
return targetEditor || sortChallengeFiles(challengeFiles ?? [])[0].fileKey;
}
@@ -1,5 +1,4 @@
const path = require('path');
const { sortChallengeFiles } = require('../sort-challengefiles');
const { viewTypes } = require('../../../shared/config/challenge-types');
const backend = path.resolve(
@@ -138,15 +137,16 @@ function getProjectPreviewConfig(challenge, allChallengeEdges) {
.filter(({ node: { challenge } }) => challenge.block === block)
.map(({ node: { challenge } }) => challenge);
const lastChallenge = challengesInBlock[challengesInBlock.length - 1];
const solutionToLastChallenge = sortChallengeFiles(
lastChallenge.solutions[0] ?? []
);
const lastChallengeFiles = sortChallengeFiles(
lastChallenge.challengeFiles ?? []
);
const projectPreviewChallengeFiles = lastChallengeFiles.map((file, id) => ({
const solutionFiles = lastChallenge.solutions[0] ?? [];
const lastChallengeFiles = lastChallenge.challengeFiles ?? [];
const findFileByKey = (key, files) =>
files.find(file => file.fileKey === key);
const projectPreviewChallengeFiles = lastChallengeFiles.map(file => ({
...file,
contents: solutionToLastChallenge[id]?.contents ?? file.contents
contents:
findFileByKey(file.fileKey, solutionFiles)?.contents ?? file.contents
}));
return {
-17
View File
@@ -1,17 +0,0 @@
exports.sortChallengeFiles = function sortChallengeFiles(challengeFiles) {
const xs = challengeFiles.slice();
xs.sort((a, b) => {
if (a.history[0] === 'index.jsx') return -1;
if (b.history[0] === 'index.jsx') return 1;
if (a.history[0] === 'index.html') return -1;
if (b.history[0] === 'index.html') return 1;
if (a.history[0] === 'styles.css') return -1;
if (b.history[0] === 'styles.css') return 1;
if (a.history[0] === 'script.js') return -1;
if (b.history[0] === 'script.js') return 1;
if (a.history[0] === 'index.ts') return -1;
if (b.history[0] === 'index.ts') return 1;
return 0;
});
return xs;
};
@@ -1,5 +1,5 @@
const { challengeFiles } = require('./__fixtures__/challenges');
const { sortChallengeFiles } = require('./sort-challengefiles');
import { challengeFiles } from './__fixtures__/challenges';
import { sortChallengeFiles } from './sort-challengefiles';
describe('sort-files', () => {
describe('sortChallengeFiles', () => {
+17
View File
@@ -0,0 +1,17 @@
export function sortChallengeFiles<File extends { fileKey: string }>(
challengeFiles: File[]
): File[] {
return challengeFiles.toSorted((a, b) => {
if (a.fileKey === 'indexjsx') return -1;
if (b.fileKey === 'indexjsx') return 1;
if (a.fileKey === 'indexhtml') return -1;
if (b.fileKey === 'indexhtml') return 1;
if (a.fileKey === 'stylescss') return -1;
if (b.fileKey === 'stylescss') return 1;
if (a.fileKey === 'scriptjs') return -1;
if (b.fileKey === 'scriptjs') return 1;
if (a.fileKey === 'indexts') return -1;
if (b.fileKey === 'indexts') return 1;
return 0;
});
}