mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor(client): simplify sort challenge files (#59179)
Co-authored-by: Naomi <accounts+github@nhcarrigan.com>
This commit is contained in:
committed by
GitHub
parent
6e1b87cc78
commit
a82316e469
@@ -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 {
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user