refactor: files{} -> challengeFiles[], and key -> fileKey (#43023)
* fix(client): fix client * fix propType and add comment * revert user.json prettification * slight type refactor and payload correction Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * update ChallengeFile type imports * add cypress test for code-storage * update test and storage epic * fix Shaun's tired brain's logic * refactor with suggestions Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * update codeReset * increate cypress timeout because firefox is slow * remove unused import to make linter happy * use focus on editor Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * use more specific seletor for cypress editor test * account for silly null challengeFiles Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -244,17 +244,11 @@ exports.createSchemaCustomization = ({ actions }) => {
|
||||
const { createTypes } = actions;
|
||||
const typeDefs = `
|
||||
type ChallengeNode implements Node {
|
||||
files: ChallengeFile
|
||||
challengeFiles: [FileContents]
|
||||
url: String
|
||||
}
|
||||
type ChallengeFile {
|
||||
indexcss: FileContents
|
||||
indexhtml: FileContents
|
||||
indexjs: FileContents
|
||||
indexjsx: FileContents
|
||||
}
|
||||
type FileContents {
|
||||
key: String
|
||||
fileKey: String
|
||||
ext: String
|
||||
name: String
|
||||
contents: String
|
||||
|
@ -5,7 +5,7 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||
import ProjectModal from '../components/SolutionViewer/ProjectModal';
|
||||
import { Spacer, Link } from '../components/helpers';
|
||||
import {
|
||||
ChallengeFileType,
|
||||
ChallengeFiles,
|
||||
CompletedChallenge,
|
||||
UserType
|
||||
} from '../redux/prop-types';
|
||||
@ -24,14 +24,14 @@ interface IShowProjectLinksProps {
|
||||
|
||||
type SolutionStateType = {
|
||||
projectTitle: string;
|
||||
files?: ChallengeFileType[] | null;
|
||||
challengeFiles: ChallengeFiles;
|
||||
solution: CompletedChallenge['solution'];
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
const initSolutionState: SolutionStateType = {
|
||||
projectTitle: '',
|
||||
files: null,
|
||||
challengeFiles: null,
|
||||
solution: null,
|
||||
isOpen: false
|
||||
};
|
||||
@ -56,16 +56,16 @@ const ShowProjectLinks = (props: IShowProjectLinksProps): JSX.Element => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { solution, githubLink, files } = completedProject;
|
||||
const { solution, githubLink, challengeFiles } = completedProject;
|
||||
const onClickHandler = () =>
|
||||
setSolutionState({
|
||||
projectTitle,
|
||||
files,
|
||||
challengeFiles,
|
||||
solution,
|
||||
isOpen: true
|
||||
});
|
||||
|
||||
if (files?.length) {
|
||||
if (challengeFiles?.length) {
|
||||
return (
|
||||
<button
|
||||
className='project-link-button-override'
|
||||
@ -163,7 +163,7 @@ const ShowProjectLinks = (props: IShowProjectLinksProps): JSX.Element => {
|
||||
name,
|
||||
user: { username }
|
||||
} = props;
|
||||
const { files, isOpen, projectTitle, solution } = solutionState;
|
||||
const { challengeFiles, isOpen, projectTitle, solution } = solutionState;
|
||||
return (
|
||||
<div>
|
||||
{t(
|
||||
@ -177,7 +177,7 @@ const ShowProjectLinks = (props: IShowProjectLinksProps): JSX.Element => {
|
||||
<Spacer />
|
||||
{isOpen ? (
|
||||
<ProjectModal
|
||||
files={files}
|
||||
challengeFiles={challengeFiles}
|
||||
handleSolutionModalHide={handleSolutionModalHide}
|
||||
isOpen={isOpen}
|
||||
projectTitle={projectTitle}
|
||||
|
@ -5,15 +5,16 @@ import { useTranslation } from 'react-i18next';
|
||||
import SolutionViewer from './SolutionViewer';
|
||||
|
||||
const propTypes = {
|
||||
files: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
contents: PropTypes.string,
|
||||
ext: PropTypes.string,
|
||||
key: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
path: PropTypes.string
|
||||
})
|
||||
),
|
||||
challengeFiles: PropTypes.array,
|
||||
// TODO: removed once refactored to TS
|
||||
// PropTypes.shape({
|
||||
// contents: PropTypes.string,
|
||||
// ext: PropTypes.string,
|
||||
// key: PropTypes.string,
|
||||
// name: PropTypes.string,
|
||||
// path: PropTypes.string
|
||||
// })
|
||||
// ),
|
||||
handleSolutionModalHide: PropTypes.func,
|
||||
isOpen: PropTypes.bool,
|
||||
projectTitle: PropTypes.string,
|
||||
@ -21,8 +22,13 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const ProjectModal = props => {
|
||||
const { isOpen, projectTitle, files, solution, handleSolutionModalHide } =
|
||||
props;
|
||||
const {
|
||||
isOpen,
|
||||
projectTitle,
|
||||
challengeFiles,
|
||||
solution,
|
||||
handleSolutionModalHide
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
@ -39,7 +45,7 @@ const ProjectModal = props => {
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<SolutionViewer files={files} solution={solution} />
|
||||
<SolutionViewer challengeFiles={challengeFiles} solution={solution} />
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={handleSolutionModalHide}>{t('buttons.close')}</Button>
|
||||
|
@ -11,21 +11,25 @@ const prismLang = {
|
||||
};
|
||||
|
||||
const SolutionViewer = ({
|
||||
files,
|
||||
challengeFiles,
|
||||
solution = '// The solution is not available for this project'
|
||||
}) =>
|
||||
files && Array.isArray(files) && files.length ? (
|
||||
files.map(file => (
|
||||
<Panel bsStyle='primary' className='solution-viewer' key={file.ext}>
|
||||
<Panel.Heading>{file.ext.toUpperCase()}</Panel.Heading>
|
||||
challengeFiles?.length ? (
|
||||
challengeFiles.map(challengeFile => (
|
||||
<Panel
|
||||
bsStyle='primary'
|
||||
className='solution-viewer'
|
||||
key={challengeFile.ext}
|
||||
>
|
||||
<Panel.Heading>{challengeFile.ext.toUpperCase()}</Panel.Heading>
|
||||
<Panel.Body>
|
||||
<pre>
|
||||
<code
|
||||
className={`language-${prismLang[file.ext]}`}
|
||||
className={`language-${prismLang[challengeFile.ext]}`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Prism.highlight(
|
||||
file.contents.trim(),
|
||||
Prism.languages[prismLang[file.ext]]
|
||||
challengeFile.contents.trim(),
|
||||
Prism.languages[prismLang[challengeFile.ext]]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
@ -59,7 +63,7 @@ const SolutionViewer = ({
|
||||
|
||||
SolutionViewer.displayName = 'SolutionViewer';
|
||||
SolutionViewer.propTypes = {
|
||||
files: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.string)),
|
||||
challengeFiles: PropTypes.array,
|
||||
solution: PropTypes.string
|
||||
};
|
||||
|
||||
|
@ -80,7 +80,7 @@ describe('<TimeLine />', () => {
|
||||
|
||||
const contents = 'This is not JS';
|
||||
const ext = 'js';
|
||||
const key = 'indexjs';
|
||||
const fileKey = 'indexjs';
|
||||
const name = 'index';
|
||||
const path = 'index.js';
|
||||
|
||||
@ -100,15 +100,7 @@ const propsForOnlySolution = {
|
||||
{
|
||||
id: '5e46f7f8ac417301a38fb92a',
|
||||
completedDate: 1604043678032,
|
||||
files: [
|
||||
{
|
||||
contents,
|
||||
ext,
|
||||
key,
|
||||
name,
|
||||
path
|
||||
}
|
||||
]
|
||||
challengeFiles: [{ contents, ext, fileKey, name, path }]
|
||||
}
|
||||
],
|
||||
username: 'developmentuser'
|
||||
|
@ -1,8 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import {
|
||||
Button,
|
||||
@ -25,6 +20,7 @@ import {
|
||||
getTitleFromId
|
||||
} from '../../../../../utils';
|
||||
import CertificationIcon from '../../../assets/icons/certification-icon';
|
||||
import { ChallengeFiles } from '../../../redux/prop-types';
|
||||
import { maybeUrlRE } from '../../../utils';
|
||||
import { FullWidthRow, Link } from '../../helpers';
|
||||
import TimelinePagination from './TimelinePagination';
|
||||
@ -32,70 +28,54 @@ import TimelinePagination from './TimelinePagination';
|
||||
import './timeline.css';
|
||||
|
||||
const SolutionViewer = Loadable(
|
||||
() =>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import('../../SolutionViewer/SolutionViewer')
|
||||
() => import('../../SolutionViewer/SolutionViewer')
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { clientLocale } = envData;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
|
||||
const { clientLocale } = envData as { clientLocale: keyof typeof langCodes };
|
||||
const localeCode = langCodes[clientLocale];
|
||||
|
||||
// Items per page in timeline.
|
||||
const ITEMS_PER_PAGE = 15;
|
||||
|
||||
interface ICompletedMap {
|
||||
interface CompletedMap {
|
||||
id: string;
|
||||
completedDate: number;
|
||||
challengeType: number;
|
||||
solution: string;
|
||||
files: IFile[];
|
||||
challengeFiles: ChallengeFiles;
|
||||
githubLink: string;
|
||||
}
|
||||
|
||||
interface ITimelineProps {
|
||||
completedMap: ICompletedMap[];
|
||||
interface TimelineProps {
|
||||
completedMap: CompletedMap[];
|
||||
t: TFunction;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface IFile {
|
||||
ext: string;
|
||||
contents: string;
|
||||
}
|
||||
|
||||
interface ISortedTimeline {
|
||||
interface SortedTimeline {
|
||||
id: string;
|
||||
completedDate: number;
|
||||
files: IFile[];
|
||||
challengeFiles: ChallengeFiles;
|
||||
githubLink: string;
|
||||
solution: string;
|
||||
}
|
||||
|
||||
interface ITimelineInnerProps extends ITimelineProps {
|
||||
interface TimelineInnerProps extends TimelineProps {
|
||||
idToNameMap: Map<string, string>;
|
||||
sortedTimeline: ISortedTimeline[];
|
||||
sortedTimeline: SortedTimeline[];
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
interface ITimeLineInnerState {
|
||||
interface TimeLineInnerState {
|
||||
solutionToView: string | null;
|
||||
solutionOpen: boolean;
|
||||
pageNo: number;
|
||||
solution: string | null;
|
||||
files: IFile[] | null;
|
||||
challengeFiles: ChallengeFiles;
|
||||
}
|
||||
|
||||
class TimelineInner extends Component<
|
||||
ITimelineInnerProps,
|
||||
ITimeLineInnerState
|
||||
> {
|
||||
constructor(props: ITimelineInnerProps) {
|
||||
class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
|
||||
constructor(props: TimelineInnerProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@ -103,7 +83,7 @@ class TimelineInner extends Component<
|
||||
solutionOpen: false,
|
||||
pageNo: 1,
|
||||
solution: null,
|
||||
files: null
|
||||
challengeFiles: null
|
||||
};
|
||||
|
||||
this.closeSolution = this.closeSolution.bind(this);
|
||||
@ -118,19 +98,19 @@ class TimelineInner extends Component<
|
||||
|
||||
renderViewButton(
|
||||
id: string,
|
||||
files: IFile[],
|
||||
challengeFiles: ChallengeFiles,
|
||||
githubLink: string,
|
||||
solution: string
|
||||
): React.ReactNode {
|
||||
const { t } = this.props;
|
||||
if (files && files.length) {
|
||||
if (challengeFiles?.length) {
|
||||
return (
|
||||
<Button
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
className='btn-invert'
|
||||
id={`btn-for-${id}`}
|
||||
onClick={() => this.viewSolution(id, solution, files)}
|
||||
onClick={() => this.viewSolution(id, solution, challengeFiles)}
|
||||
>
|
||||
{t('buttons.show-code')}
|
||||
</Button>
|
||||
@ -183,12 +163,11 @@ class TimelineInner extends Component<
|
||||
}
|
||||
}
|
||||
|
||||
renderCompletion(completed: ISortedTimeline): JSX.Element {
|
||||
renderCompletion(completed: SortedTimeline): JSX.Element {
|
||||
const { idToNameMap, username } = this.props;
|
||||
const { id, files, githubLink, solution } = completed;
|
||||
const { id, challengeFiles, githubLink, solution } = completed;
|
||||
const completedDate = new Date(completed.completedDate);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// @ts-expect-error idToNameMap is not a <string, string> Map...
|
||||
const { challengeTitle, challengePath, certPath } = idToNameMap.get(id);
|
||||
return (
|
||||
<tr className='timeline-row' key={id}>
|
||||
@ -196,16 +175,18 @@ class TimelineInner extends Component<
|
||||
{certPath ? (
|
||||
<Link
|
||||
className='timeline-cert-link'
|
||||
to={`/certification/${username}/${certPath}`}
|
||||
to={`/certification/${username}/${certPath as string}`}
|
||||
>
|
||||
{challengeTitle}
|
||||
<CertificationIcon />
|
||||
</Link>
|
||||
) : (
|
||||
<Link to={challengePath}>{challengeTitle}</Link>
|
||||
<Link to={challengePath as string}>{challengeTitle}</Link>
|
||||
)}
|
||||
</td>
|
||||
<td>{this.renderViewButton(id, files, githubLink, solution)}</td>
|
||||
<td>
|
||||
{this.renderViewButton(id, challengeFiles, githubLink, solution)}
|
||||
</td>
|
||||
<td className='text-center'>
|
||||
<time dateTime={completedDate.toISOString()}>
|
||||
{completedDate.toLocaleString([localeCode, 'en-US'], {
|
||||
@ -218,13 +199,17 @@ class TimelineInner extends Component<
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
viewSolution(id: string, solution: string, files: IFile[]): void {
|
||||
viewSolution(
|
||||
id: string,
|
||||
solution: string,
|
||||
challengeFiles: ChallengeFiles
|
||||
): void {
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
solutionToView: id,
|
||||
solutionOpen: true,
|
||||
solution,
|
||||
files
|
||||
challengeFiles
|
||||
}));
|
||||
}
|
||||
|
||||
@ -234,7 +219,7 @@ class TimelineInner extends Component<
|
||||
solutionToView: null,
|
||||
solutionOpen: false,
|
||||
solution: null,
|
||||
files: null
|
||||
challengeFiles: null
|
||||
}));
|
||||
}
|
||||
|
||||
@ -306,15 +291,14 @@ class TimelineInner extends Component<
|
||||
<Modal.Title id='contained-modal-title'>
|
||||
{`${username}'s Solution to ${
|
||||
// @ts-expect-error Need better TypeDef for this
|
||||
idToNameMap.get(id).challengeTitle
|
||||
idToNameMap.get(id).challengeTitle as string
|
||||
}`}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<SolutionViewer
|
||||
// @ts-expect-error Need Better TypeDef
|
||||
files={this.state.files}
|
||||
solution={this.state.solution}
|
||||
challengeFiles={this.state.challengeFiles}
|
||||
solution={this.state.solution ?? ''}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
@ -336,7 +320,7 @@ class TimelineInner extends Component<
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call*/
|
||||
function useIdToNameMap(): Map<string, string> {
|
||||
const {
|
||||
allChallengeNode: { edges }
|
||||
@ -365,22 +349,22 @@ function useIdToNameMap(): Map<string, string> {
|
||||
edges.forEach(
|
||||
({
|
||||
node: {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error Graphql needs typing
|
||||
id,
|
||||
// @ts-ignore
|
||||
// @ts-expect-error Graphql needs typing
|
||||
title,
|
||||
// @ts-ignore
|
||||
// @ts-expect-error Graphql needs typing
|
||||
fields: { slug }
|
||||
}
|
||||
}) => {
|
||||
idToNameMap.set(id, { challengeTitle: title, challengePath: slug });
|
||||
}
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return idToNameMap;
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
const Timeline = (props: ITimelineProps): JSX.Element => {
|
||||
const Timeline = (props: TimelineProps): JSX.Element => {
|
||||
const idToNameMap = useIdToNameMap();
|
||||
const { completedMap } = props;
|
||||
// Get the sorted timeline along with total page count.
|
||||
|
@ -32,7 +32,7 @@ const propTypes = {
|
||||
githubLink: PropTypes.string,
|
||||
challengeType: PropTypes.number,
|
||||
completedDate: PropTypes.number,
|
||||
files: PropTypes.array
|
||||
challengeFiles: PropTypes.array
|
||||
})
|
||||
),
|
||||
createFlashMessage: PropTypes.func.isRequired,
|
||||
@ -136,7 +136,7 @@ const honestyInfoMessage = {
|
||||
const initialState = {
|
||||
solutionViewer: {
|
||||
projectTitle: '',
|
||||
files: null,
|
||||
challengeFiles: null,
|
||||
solution: null,
|
||||
isOpen: false
|
||||
}
|
||||
@ -167,17 +167,17 @@ export class CertificationSettings extends Component {
|
||||
if (!completedProject) {
|
||||
return null;
|
||||
}
|
||||
const { solution, githubLink, files } = completedProject;
|
||||
const { solution, githubLink, challengeFiles } = completedProject;
|
||||
const onClickHandler = () =>
|
||||
this.setState({
|
||||
solutionViewer: {
|
||||
projectTitle,
|
||||
files,
|
||||
challengeFiles,
|
||||
solution,
|
||||
isOpen: true
|
||||
}
|
||||
});
|
||||
if (files && files.length) {
|
||||
if (challengeFiles?.length) {
|
||||
return (
|
||||
<Button
|
||||
block={true}
|
||||
@ -417,7 +417,7 @@ export class CertificationSettings extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
solutionViewer: { files, solution, isOpen, projectTitle }
|
||||
solutionViewer: { challengeFiles, solution, isOpen, projectTitle }
|
||||
} = this.state;
|
||||
|
||||
const { t } = this.props;
|
||||
@ -434,7 +434,7 @@ export class CertificationSettings extends Component {
|
||||
)}
|
||||
{isOpen ? (
|
||||
<ProjectModal
|
||||
files={files}
|
||||
challengeFiles={challengeFiles}
|
||||
handleSolutionModalHide={this.handleSolutionModalHide}
|
||||
isOpen={isOpen}
|
||||
projectTitle={projectTitle}
|
||||
|
@ -241,7 +241,7 @@ const defaultTestProps = {
|
||||
|
||||
const contents = 'This is not JS';
|
||||
const ext = 'js';
|
||||
const key = 'indexjs';
|
||||
const fileKey = 'indexjs';
|
||||
const name = 'index';
|
||||
const path = 'index.js';
|
||||
|
||||
@ -259,11 +259,11 @@ const propsForOnlySolution = {
|
||||
},
|
||||
{
|
||||
id: '5e46f7f8ac417301a38fb92a',
|
||||
files: [
|
||||
challengeFiles: [
|
||||
{
|
||||
contents,
|
||||
ext,
|
||||
key,
|
||||
fileKey,
|
||||
name,
|
||||
path
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { HandlerProps } from 'react-reflex';
|
||||
|
||||
const FileType = PropTypes.shape({
|
||||
export const FileType = PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
ext: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
@ -24,10 +25,7 @@ export const ChallengeNode = PropTypes.shape({
|
||||
challengeType: PropTypes.number,
|
||||
dashedName: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
files: PropTypes.shape({
|
||||
indexhtml: FileType,
|
||||
indexjs: FileType
|
||||
}),
|
||||
challengeFiles: PropTypes.array,
|
||||
fields: PropTypes.shape({
|
||||
slug: PropTypes.string,
|
||||
blockName: PropTypes.string
|
||||
@ -83,7 +81,7 @@ export const User = PropTypes.shape({
|
||||
githubLink: PropTypes.string,
|
||||
challengeType: PropTypes.number,
|
||||
completedDate: PropTypes.number,
|
||||
files: PropTypes.array
|
||||
challengeFiles: PropTypes.array
|
||||
})
|
||||
),
|
||||
email: PropTypes.string,
|
||||
@ -176,19 +174,24 @@ export type MarkdownRemarkType = {
|
||||
words: number;
|
||||
};
|
||||
};
|
||||
|
||||
type Question = { text: string; answers: string[]; solution: number };
|
||||
type Fields = { slug: string; blockName: string; tests: Test[] };
|
||||
type Required = {
|
||||
link: string;
|
||||
raw: boolean;
|
||||
src: string;
|
||||
crossDomain?: boolean;
|
||||
};
|
||||
|
||||
export type ChallengeNodeType = {
|
||||
block: string;
|
||||
challengeOrder: number;
|
||||
challengeType: number;
|
||||
dashedName: string;
|
||||
description: string;
|
||||
challengeFiles: ChallengeFileType[];
|
||||
fields: {
|
||||
slug: string;
|
||||
blockName: string;
|
||||
tests: TestType[];
|
||||
};
|
||||
files: ChallengeFileType;
|
||||
challengeFiles: ChallengeFiles;
|
||||
fields: Fields;
|
||||
forumTopicId: number;
|
||||
guideUrl: string;
|
||||
head: string[];
|
||||
@ -200,18 +203,8 @@ export type ChallengeNodeType = {
|
||||
isLocked: boolean;
|
||||
isPrivate: boolean;
|
||||
order: number;
|
||||
question: {
|
||||
text: string;
|
||||
answers: string[];
|
||||
solution: number;
|
||||
};
|
||||
required: [
|
||||
{
|
||||
link: string;
|
||||
raw: string;
|
||||
src: string;
|
||||
}
|
||||
];
|
||||
question: Question;
|
||||
required: Required[];
|
||||
superOrder: number;
|
||||
superBlock: string;
|
||||
tail: string[];
|
||||
@ -240,7 +233,7 @@ export type AllMarkdownRemarkType = {
|
||||
};
|
||||
|
||||
export type ResizePropsType = {
|
||||
onStopResize: (arg0: React.ChangeEvent) => void;
|
||||
onStopResize: (arg0: HandlerProps) => void;
|
||||
onResize: () => void;
|
||||
};
|
||||
|
||||
@ -249,11 +242,19 @@ export type DimensionsType = {
|
||||
width: number;
|
||||
};
|
||||
|
||||
export type TestType = {
|
||||
text: string;
|
||||
testString: string;
|
||||
export type Test = {
|
||||
pass?: boolean;
|
||||
err?: string;
|
||||
} & (ChallengeTest | CertTest);
|
||||
|
||||
export type ChallengeTest = {
|
||||
text: string;
|
||||
testString: string;
|
||||
};
|
||||
|
||||
export type CertTest = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type UserType = {
|
||||
@ -311,39 +312,12 @@ export type CompletedChallenge = {
|
||||
githubLink?: string;
|
||||
challengeType?: number;
|
||||
completedDate: number;
|
||||
challengeFiles: ChallengeFileType[] | null;
|
||||
// TODO: remove once files->challengeFiles is refactored
|
||||
files?: ChallengeFileType[] | null;
|
||||
challengeFiles: ChallengeFiles;
|
||||
};
|
||||
// TODO: renames: files => challengeFiles; key => fileKey; #42489
|
||||
export type ChallengeFileType =
|
||||
| {
|
||||
[T in FileKeyTypes]:
|
||||
| ({
|
||||
editableContents: string;
|
||||
editableRegionBoundaries: number[];
|
||||
error?: string | null;
|
||||
history: string[];
|
||||
path: string;
|
||||
seed: string;
|
||||
seedEditableRegionBoundaries?: number[];
|
||||
} & FileKeyChallengeType)
|
||||
| null;
|
||||
}
|
||||
| Record<string, never>;
|
||||
|
||||
export type ExtTypes = 'js' | 'html' | 'css' | 'jsx';
|
||||
export type FileKeyTypes = 'indexjs' | 'indexhtml' | 'indexcss';
|
||||
|
||||
export type ChallengeFilesType =
|
||||
| {
|
||||
indexcss: ChallengeFileType;
|
||||
indexhtml: ChallengeFileType;
|
||||
indexjs: ChallengeFileType;
|
||||
indexjsx: ChallengeFileType;
|
||||
}
|
||||
| Record<string, never>;
|
||||
|
||||
export type ChallengeMetaType = {
|
||||
block: string;
|
||||
id: string;
|
||||
@ -383,7 +357,7 @@ export type FileKeyChallengeType = {
|
||||
// think are on the node, but actually do not exist.
|
||||
export type ChallengeNode = {
|
||||
block: string;
|
||||
challengeFiles: ChallengeFileType;
|
||||
challengeFiles: ChallengeFiles;
|
||||
challengeOrder: number;
|
||||
challengeType: number;
|
||||
dashedName: string;
|
||||
@ -391,7 +365,7 @@ export type ChallengeNode = {
|
||||
fields: {
|
||||
slug: string;
|
||||
blockName: string;
|
||||
tests: TestType[];
|
||||
tests: Test[];
|
||||
};
|
||||
forumTopicId: number;
|
||||
// guideUrl: string;
|
||||
@ -430,7 +404,7 @@ export type ChallengeNode = {
|
||||
superBlock: string;
|
||||
superOrder: number;
|
||||
template: string;
|
||||
tests: TestType[];
|
||||
tests: Test[];
|
||||
time: string;
|
||||
title: string;
|
||||
translationPending: boolean;
|
||||
@ -441,3 +415,61 @@ export type ChallengeNode = {
|
||||
// isPrivate: boolean;
|
||||
// tail: string[];
|
||||
};
|
||||
|
||||
// Extra types built from challengeSchema
|
||||
|
||||
export type ChallengeFile = {
|
||||
fileKey: string;
|
||||
ext: ExtTypes;
|
||||
name: string;
|
||||
editableRegionBoundaries: number[];
|
||||
path: string;
|
||||
error: null | string;
|
||||
head: string;
|
||||
tail: string;
|
||||
seed: string;
|
||||
contents: string;
|
||||
id: string;
|
||||
history: [[string], string];
|
||||
};
|
||||
|
||||
export type ChallengeFiles = ChallengeFile[] | null;
|
||||
|
||||
export interface ChallengeSchema {
|
||||
block: string;
|
||||
blockId: string;
|
||||
challengeOrder: number;
|
||||
removeComments: boolean;
|
||||
// TODO: should be typed with possible values
|
||||
challengeType: number;
|
||||
checksum: number;
|
||||
__commentCounts: Record<string, unknown>;
|
||||
dashedName: string;
|
||||
description: string;
|
||||
challengeFiles: ChallengeFiles;
|
||||
guideUrl: string;
|
||||
// TODO: should be typed with possible values
|
||||
helpCategory: string;
|
||||
videoUrl: string;
|
||||
forumTopicId: number;
|
||||
id: string;
|
||||
instructions: string;
|
||||
isComingSoon: boolean;
|
||||
// TODO: Do we still use this
|
||||
isLocked: boolean;
|
||||
isPrivate: boolean;
|
||||
order: number;
|
||||
videoId?: string;
|
||||
question: Question;
|
||||
required: Required[];
|
||||
solutions: ChallengeFile[][];
|
||||
superBlock: string;
|
||||
superOrder: number;
|
||||
suborder: number;
|
||||
tests: Test[];
|
||||
template: string;
|
||||
time: string;
|
||||
title: string;
|
||||
translationPending: boolean;
|
||||
url?: string;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ const paneType = {
|
||||
|
||||
const propTypes = {
|
||||
block: PropTypes.string,
|
||||
challengeFiles: PropTypes.object,
|
||||
challengeFiles: PropTypes.array,
|
||||
editor: PropTypes.element,
|
||||
hasEditableBoundries: PropTypes.bool,
|
||||
hasPreview: PropTypes.bool,
|
||||
@ -58,7 +58,7 @@ class DesktopLayout extends Component {
|
||||
|
||||
getChallengeFile() {
|
||||
const { challengeFiles } = this.props;
|
||||
return first(Object.keys(challengeFiles).map(key => challengeFiles[key]));
|
||||
return first(challengeFiles);
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -121,7 +121,7 @@ class DesktopLayout extends Component {
|
||||
!hasEditableBoundries && <EditorTabs />}
|
||||
{challengeFile && (
|
||||
<ReflexContainer
|
||||
key={challengeFile.key}
|
||||
key={challengeFile.fileKey}
|
||||
orientation='horizontal'
|
||||
>
|
||||
<ReflexElement
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from '../redux';
|
||||
|
||||
const propTypes = {
|
||||
challengeFiles: PropTypes.object.isRequired,
|
||||
challengeFiles: PropTypes.array.isRequired,
|
||||
toggleVisibleEditor: PropTypes.func.isRequired,
|
||||
visibleEditors: PropTypes.shape({
|
||||
indexjs: PropTypes.bool,
|
||||
@ -38,46 +38,17 @@ class EditorTabs extends Component {
|
||||
const { challengeFiles, toggleVisibleEditor, visibleEditors } = this.props;
|
||||
return (
|
||||
<div className='monaco-editor-tabs'>
|
||||
{challengeFiles['indexjsx'] && (
|
||||
{challengeFiles.map(challengeFile => (
|
||||
<button
|
||||
aria-selected={visibleEditors.indexjsx}
|
||||
aria-selected={visibleEditors[challengeFile.fileKey]}
|
||||
className='monaco-editor-tab'
|
||||
onClick={() => toggleVisibleEditor('indexjsx')}
|
||||
key={challengeFile.fileKey}
|
||||
onClick={() => toggleVisibleEditor(challengeFile.fileKey)}
|
||||
role='tab'
|
||||
>
|
||||
script.jsx
|
||||
{challengeFile.path}
|
||||
</button>
|
||||
)}
|
||||
{challengeFiles['indexhtml'] && (
|
||||
<button
|
||||
aria-selected={visibleEditors.indexhtml}
|
||||
className='monaco-editor-tab'
|
||||
onClick={() => toggleVisibleEditor('indexhtml')}
|
||||
role='tab'
|
||||
>
|
||||
index.html
|
||||
</button>
|
||||
)}
|
||||
{challengeFiles['indexcss'] && (
|
||||
<button
|
||||
aria-selected={visibleEditors.indexcss}
|
||||
className='monaco-editor-tab'
|
||||
onClick={() => toggleVisibleEditor('indexcss')}
|
||||
role='tab'
|
||||
>
|
||||
styles.css
|
||||
</button>
|
||||
)}
|
||||
{challengeFiles['indexjs'] && (
|
||||
<button
|
||||
aria-selected={visibleEditors.indexjs}
|
||||
className='monaco-editor-tab'
|
||||
onClick={() => toggleVisibleEditor('indexjs')}
|
||||
role='tab'
|
||||
>
|
||||
script.js
|
||||
</button>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import Editor from './editor';
|
||||
const propTypes = {
|
||||
canFocus: PropTypes.bool,
|
||||
// TODO: use shape
|
||||
challengeFiles: PropTypes.object,
|
||||
challengeFiles: PropTypes.array,
|
||||
containerRef: PropTypes.any.isRequired,
|
||||
contents: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
@ -39,6 +39,7 @@ const propTypes = {
|
||||
saveEditorContent: PropTypes.func.isRequired,
|
||||
setEditorFocusability: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
// TODO: is this used?
|
||||
title: PropTypes.string,
|
||||
updateFile: PropTypes.func.isRequired,
|
||||
visibleEditors: PropTypes.shape({
|
||||
|
@ -1,12 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
// Package Utilities
|
||||
import { graphql } from 'gatsby';
|
||||
import React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { TFunction, withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { HandlerProps } from 'react-reflex';
|
||||
import Media from 'react-responsive';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
@ -17,9 +15,10 @@ import { challengeTypes } from '../../../../utils/challenge-types';
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import {
|
||||
ChallengeNodeType,
|
||||
ChallengeFileType,
|
||||
ChallengeFiles,
|
||||
ChallengeFile,
|
||||
ChallengeMetaType,
|
||||
TestType,
|
||||
Test,
|
||||
ResizePropsType
|
||||
} from '../../../redux/prop-types';
|
||||
import { isContained } from '../../../utils/is-contained';
|
||||
@ -57,7 +56,7 @@ import '../components/test-frame.css';
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
files: challengeFilesSelector,
|
||||
challengeFiles: challengeFilesSelector,
|
||||
tests: challengeTestsSelector,
|
||||
output: consoleOutputSelector,
|
||||
isChallengeCompleted: isChallengeCompletedSelector
|
||||
@ -81,28 +80,28 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
interface ShowClassicProps {
|
||||
cancelTests: () => void;
|
||||
challengeMounted: (arg0: string) => void;
|
||||
createFiles: (arg0: ChallengeFileType) => void;
|
||||
createFiles: (arg0: ChallengeFile[]) => void;
|
||||
data: { challengeNode: ChallengeNodeType };
|
||||
executeChallenge: () => void;
|
||||
files: ChallengeFileType;
|
||||
challengeFiles: ChallengeFiles;
|
||||
initConsole: (arg0: string) => void;
|
||||
initTests: (tests: TestType[]) => void;
|
||||
initTests: (tests: Test[]) => void;
|
||||
isChallengeCompleted: boolean;
|
||||
output: string[];
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMetaType;
|
||||
};
|
||||
t: TFunction;
|
||||
tests: TestType[];
|
||||
tests: Test[];
|
||||
updateChallengeMeta: (arg0: ChallengeMetaType) => void;
|
||||
}
|
||||
|
||||
interface ShowClassicState {
|
||||
layout: IReflexLayout | string;
|
||||
layout: ReflexLayout | string;
|
||||
resizing: boolean;
|
||||
}
|
||||
|
||||
interface IReflexLayout {
|
||||
interface ReflexLayout {
|
||||
codePane: { flex: number };
|
||||
editorPane: { flex: number };
|
||||
instructionPane: { flex: number };
|
||||
@ -147,8 +146,9 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
this.instructionsPanelRef = React.createRef();
|
||||
}
|
||||
|
||||
getLayoutState(): IReflexLayout | string {
|
||||
const reflexLayout: IReflexLayout | string = store.get(REFLEX_LAYOUT);
|
||||
getLayoutState(): ReflexLayout | string {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const reflexLayout: ReflexLayout | string = store.get(REFLEX_LAYOUT);
|
||||
|
||||
// Validate if user has not done any resize of the panes
|
||||
if (!reflexLayout) return BASE_LAYOUT;
|
||||
@ -168,8 +168,8 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
this.setState(state => ({ ...state, resizing: true }));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onStopResize(event: any) {
|
||||
onStopResize(event: HandlerProps) {
|
||||
// @ts-expect-error TODO: Apparently, name does not exist on type
|
||||
const { name, flex } = event.component.props;
|
||||
|
||||
// Only interested in tracking layout updates for ReflexElement's
|
||||
@ -236,7 +236,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
updateChallengeMeta,
|
||||
data: {
|
||||
challengeNode: {
|
||||
files,
|
||||
challengeFiles,
|
||||
fields: { tests },
|
||||
challengeType,
|
||||
removeComments,
|
||||
@ -246,7 +246,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
pageContext: { challengeMeta }
|
||||
} = this.props;
|
||||
initConsole('');
|
||||
createFiles(files);
|
||||
createFiles(challengeFiles ?? []);
|
||||
initTests(tests);
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
@ -260,7 +260,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
|
||||
componentWillUnmount() {
|
||||
const { createFiles, cancelTests } = this.props;
|
||||
createFiles({});
|
||||
createFiles([]);
|
||||
cancelTests();
|
||||
}
|
||||
|
||||
@ -319,13 +319,13 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
}
|
||||
|
||||
renderEditor() {
|
||||
const { files } = this.props;
|
||||
const { challengeFiles } = this.props;
|
||||
const { description, title } = this.getChallenge();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return (
|
||||
files && (
|
||||
challengeFiles && (
|
||||
<MultifileEditor
|
||||
challengeFiles={files}
|
||||
challengeFiles={challengeFiles}
|
||||
containerRef={this.containerRef}
|
||||
description={description}
|
||||
editorRef={this.editorRef}
|
||||
@ -358,11 +358,11 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
}
|
||||
|
||||
hasEditableBoundries() {
|
||||
const { files } = this.props;
|
||||
return Object.values(files).some(
|
||||
file =>
|
||||
file?.editableRegionBoundaries &&
|
||||
file.editableRegionBoundaries.length === 2
|
||||
const { challengeFiles } = this.props;
|
||||
return (
|
||||
challengeFiles?.some(
|
||||
challengeFile => challengeFile.editableRegionBoundaries?.length === 2
|
||||
) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
@ -379,7 +379,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
pageContext: {
|
||||
challengeMeta: { nextChallengePath, prevChallengePath }
|
||||
},
|
||||
files,
|
||||
challengeFiles,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
@ -414,7 +414,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
<Media minWidth={MAX_MOBILE_WIDTH + 1}>
|
||||
<DesktopLayout
|
||||
block={block}
|
||||
challengeFiles={files}
|
||||
challengeFiles={challengeFiles}
|
||||
editor={this.renderEditor()}
|
||||
hasEditableBoundries={this.hasEditableBoundries()}
|
||||
hasPreview={this.hasPreview()}
|
||||
@ -478,9 +478,8 @@ export const query = graphql`
|
||||
link
|
||||
src
|
||||
}
|
||||
files {
|
||||
indexcss {
|
||||
key
|
||||
challengeFiles {
|
||||
fileKey
|
||||
ext
|
||||
name
|
||||
contents
|
||||
@ -488,34 +487,6 @@ export const query = graphql`
|
||||
tail
|
||||
editableRegionBoundaries
|
||||
}
|
||||
indexhtml {
|
||||
key
|
||||
ext
|
||||
name
|
||||
contents
|
||||
head
|
||||
tail
|
||||
editableRegionBoundaries
|
||||
}
|
||||
indexjs {
|
||||
key
|
||||
ext
|
||||
name
|
||||
contents
|
||||
head
|
||||
tail
|
||||
editableRegionBoundaries
|
||||
}
|
||||
indexjsx {
|
||||
key
|
||||
ext
|
||||
name
|
||||
contents
|
||||
head
|
||||
tail
|
||||
editableRegionBoundaries
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -20,12 +20,12 @@ import store from 'store';
|
||||
import { Loader } from '../../../components/helpers';
|
||||
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
|
||||
import {
|
||||
ChallengeFileType,
|
||||
ChallengeFiles,
|
||||
DimensionsType,
|
||||
ExtTypes,
|
||||
FileKeyTypes,
|
||||
ResizePropsType,
|
||||
TestType
|
||||
Test
|
||||
} from '../../../redux/prop-types';
|
||||
|
||||
import {
|
||||
@ -45,7 +45,7 @@ const MonacoEditor = Loadable(() => import('react-monaco-editor'));
|
||||
|
||||
interface EditorProps {
|
||||
canFocus: boolean;
|
||||
challengeFiles: ChallengeFileType;
|
||||
challengeFiles: ChallengeFiles;
|
||||
containerRef: RefObject<HTMLElement>;
|
||||
contents: string;
|
||||
description: string;
|
||||
@ -61,11 +61,11 @@ interface EditorProps {
|
||||
saveEditorContent: () => void;
|
||||
setEditorFocusability: (isFocusable: boolean) => void;
|
||||
submitChallenge: () => void;
|
||||
tests: TestType[];
|
||||
tests: Test[];
|
||||
theme: string;
|
||||
title: string;
|
||||
updateFile: (objest: {
|
||||
key: FileKeyTypes;
|
||||
updateFile: (object: {
|
||||
fileKey: FileKeyTypes;
|
||||
editorValue: string;
|
||||
editableRegionBoundaries: number[] | null;
|
||||
}) => void;
|
||||
@ -242,7 +242,9 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
|
||||
const getEditableRegion = () => {
|
||||
const { challengeFiles, fileKey } = props;
|
||||
const edRegBounds = challengeFiles[fileKey]?.editableRegionBoundaries;
|
||||
const edRegBounds = challengeFiles?.find(
|
||||
challengeFile => challengeFile.fileKey === fileKey
|
||||
)?.editableRegionBoundaries;
|
||||
return edRegBounds ? [...edRegBounds] : [];
|
||||
};
|
||||
|
||||
@ -256,11 +258,14 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
// swap and reuse models, we have to create our own models to prevent
|
||||
// disposal.
|
||||
|
||||
const challengeFile = challengeFiles?.find(
|
||||
challengeFile => challengeFile.fileKey === fileKey
|
||||
);
|
||||
const model =
|
||||
data.model ||
|
||||
monaco.editor.createModel(
|
||||
challengeFiles[fileKey]?.contents ?? '',
|
||||
modeMap[challengeFiles[fileKey]?.ext ?? 'html']
|
||||
challengeFile?.contents ?? '',
|
||||
modeMap[challengeFile?.ext ?? 'html']
|
||||
);
|
||||
data.model = model;
|
||||
const editableRegion = getEditableRegion();
|
||||
@ -277,7 +282,9 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
const { challengeFiles, fileKey } = props;
|
||||
const { model } = dataRef.current[fileKey];
|
||||
|
||||
const newContents = challengeFiles[fileKey]?.contents;
|
||||
const newContents = challengeFiles?.find(
|
||||
challengeFile => challengeFile.fileKey === fileKey
|
||||
)?.contents;
|
||||
if (model?.getValue() !== newContents) {
|
||||
model?.setValue(newContents ?? '');
|
||||
}
|
||||
@ -576,9 +583,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
}
|
||||
|
||||
const onChange = (editorValue: string) => {
|
||||
const { updateFile } = props;
|
||||
// TODO: use fileKey everywhere?
|
||||
const { fileKey: key } = props;
|
||||
const { updateFile, fileKey } = props;
|
||||
// TODO: now that we have getCurrentEditableRegion, should the overlays
|
||||
// follow that directly? We could subscribe to changes to that and redraw if
|
||||
// those imply that the positions have changed (i.e. if the content height
|
||||
@ -589,7 +594,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
editableRegion.startLineNumber - 1,
|
||||
editableRegion.endLineNumber + 1
|
||||
];
|
||||
updateFile({ key, editorValue, editableRegionBoundaries });
|
||||
updateFile({ fileKey, editorValue, editableRegionBoundaries });
|
||||
};
|
||||
|
||||
function showEditableRegion(editableBoundaries: number[]) {
|
||||
|
@ -16,7 +16,10 @@ import {
|
||||
executeGA,
|
||||
allowBlockDonationRequests
|
||||
} from '../../../redux';
|
||||
import { AllChallengeNodeType } from '../../../redux/prop-types';
|
||||
import {
|
||||
AllChallengeNodeType,
|
||||
ChallengeFiles
|
||||
} from '../../../redux/prop-types';
|
||||
|
||||
import {
|
||||
closeModal,
|
||||
@ -39,14 +42,14 @@ const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
successMessageSelector,
|
||||
(
|
||||
files: Record<string, unknown>,
|
||||
challengeFiles: ChallengeFiles,
|
||||
{ title, id }: { title: string; id: string },
|
||||
completedChallengesIds: string[],
|
||||
isOpen: boolean,
|
||||
isSignedIn: boolean,
|
||||
message: string
|
||||
) => ({
|
||||
files,
|
||||
challengeFiles,
|
||||
title,
|
||||
id,
|
||||
completedChallengesIds,
|
||||
@ -98,7 +101,7 @@ interface CompletionModalsProps {
|
||||
completedChallengesIds: string[];
|
||||
currentBlockIds?: string[];
|
||||
executeGA: () => void;
|
||||
files: Record<string, unknown>;
|
||||
challengeFiles: ChallengeFiles;
|
||||
id: string;
|
||||
isOpen: boolean;
|
||||
isSignedIn: boolean;
|
||||
@ -133,7 +136,7 @@ export class CompletionModalInner extends Component<
|
||||
props: CompletionModalsProps,
|
||||
state: CompletionModalInnerState
|
||||
): CompletionModalInnerState {
|
||||
const { files, isOpen } = props;
|
||||
const { challengeFiles, isOpen } = props;
|
||||
if (!isOpen) {
|
||||
return { downloadURL: null, completedPercent: 0 };
|
||||
}
|
||||
@ -142,16 +145,14 @@ export class CompletionModalInner extends Component<
|
||||
URL.revokeObjectURL(downloadURL);
|
||||
}
|
||||
let newURL = null;
|
||||
const fileKeys = Object.keys(files);
|
||||
if (fileKeys.length) {
|
||||
const filesForDownload = fileKeys
|
||||
.map(key => files[key])
|
||||
if (challengeFiles?.length) {
|
||||
const filesForDownload = challengeFiles
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.reduce<string>((allFiles, currentFile: any) => {
|
||||
const beforeText = `** start of ${currentFile.path} **\n\n`;
|
||||
const afterText = `\n\n** end of ${currentFile.path} **\n\n`;
|
||||
allFiles +=
|
||||
fileKeys.length > 1
|
||||
challengeFiles.length > 1
|
||||
? `${beforeText}${currentFile.contents}${afterText}`
|
||||
: currentFile.contents;
|
||||
return allFiles;
|
||||
|
@ -14,10 +14,11 @@ import { createSelector } from 'reselect';
|
||||
import Spacer from '../../../../components/helpers/spacer';
|
||||
import LearnLayout from '../../../../components/layouts/learn';
|
||||
import { isSignedInSelector } from '../../../../redux';
|
||||
|
||||
import {
|
||||
ChallengeNodeType,
|
||||
ChallengeMetaType,
|
||||
TestType
|
||||
Test
|
||||
} from '../../../../redux/prop-types';
|
||||
import ChallengeDescription from '../../components/Challenge-Description';
|
||||
import HelpModal from '../../components/HelpModal';
|
||||
@ -52,7 +53,7 @@ const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
(
|
||||
output: string[],
|
||||
tests: TestType[],
|
||||
tests: Test[],
|
||||
isChallengeCompleted: boolean,
|
||||
isSignedIn: boolean
|
||||
) => ({
|
||||
@ -81,7 +82,7 @@ interface BackEndProps {
|
||||
forumTopicId: number;
|
||||
id: string;
|
||||
initConsole: () => void;
|
||||
initTests: (tests: TestType[]) => void;
|
||||
initTests: (tests: Test[]) => void;
|
||||
isChallengeCompleted: boolean;
|
||||
isSignedIn: boolean;
|
||||
output: string[];
|
||||
@ -89,7 +90,7 @@ interface BackEndProps {
|
||||
challengeMeta: ChallengeMetaType;
|
||||
};
|
||||
t: TFunction;
|
||||
tests: TestType[];
|
||||
tests: Test[];
|
||||
title: string;
|
||||
updateChallengeMeta: (arg0: ChallengeMetaType) => void;
|
||||
updateSolutionFormValues: () => void;
|
||||
|
@ -56,19 +56,25 @@ export const cssToHtml = cond([
|
||||
[stubTrue, identity]
|
||||
]);
|
||||
|
||||
export function findIndexHtml(files) {
|
||||
const filtered = files.filter(file => wasHtmlFile(file));
|
||||
export function findIndexHtml(challengeFiles) {
|
||||
const filtered = challengeFiles.filter(challengeFile =>
|
||||
wasHtmlFile(challengeFile)
|
||||
);
|
||||
if (filtered.length > 1) {
|
||||
throw new Error('Too many html blocks in the challenge seed');
|
||||
}
|
||||
return filtered[0];
|
||||
}
|
||||
|
||||
function wasHtmlFile(file) {
|
||||
return file.history[0] === 'index.html';
|
||||
function wasHtmlFile(challengeFile) {
|
||||
return challengeFile.history[0] === 'index.html';
|
||||
}
|
||||
|
||||
export function concatHtml({ required = [], template, files = [] } = {}) {
|
||||
export function concatHtml({
|
||||
required = [],
|
||||
template,
|
||||
challengeFiles = []
|
||||
} = {}) {
|
||||
const createBody = template ? _template(template) : defaultTemplate;
|
||||
const head = required
|
||||
.map(({ link, src }) => {
|
||||
@ -87,15 +93,15 @@ A required file can not have both a src and a link: src = ${src}, link = ${link}
|
||||
})
|
||||
.reduce((head, element) => head.concat(element));
|
||||
|
||||
const indexHtml = findIndexHtml(files);
|
||||
const indexHtml = findIndexHtml(challengeFiles);
|
||||
|
||||
const source = files.reduce((source, file) => {
|
||||
if (!indexHtml) return source.concat(file.contents, htmlCatch);
|
||||
const source = challengeFiles.reduce((source, challengeFile) => {
|
||||
if (!indexHtml) return source.concat(challengeFile.contents, htmlCatch);
|
||||
if (
|
||||
indexHtml.importedFiles.includes(file.history[0]) ||
|
||||
wasHtmlFile(file)
|
||||
indexHtml.importedFiles.includes(challengeFile.history[0]) ||
|
||||
wasHtmlFile(challengeFile)
|
||||
) {
|
||||
return source.concat(file.contents, htmlCatch);
|
||||
return source.concat(challengeFile.contents, htmlCatch);
|
||||
} else {
|
||||
return source;
|
||||
}
|
||||
|
@ -47,17 +47,22 @@ function getLegacyCode(legacy) {
|
||||
}, null);
|
||||
}
|
||||
|
||||
function legacyToFile(code, files, key) {
|
||||
if (isFilesAllPoly(files)) {
|
||||
return { [key]: setContent(code, files[key]) };
|
||||
function legacyToFile(code, challengeFiles, fileKey) {
|
||||
if (isFilesAllPoly(challengeFiles)) {
|
||||
return {
|
||||
...setContent(
|
||||
code,
|
||||
challengeFiles.find(x => x.fileKey === fileKey)
|
||||
)
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isFilesAllPoly(files) {
|
||||
return Object.keys(files)
|
||||
.map(key => files[key])
|
||||
.every(file => isPoly(file));
|
||||
function isFilesAllPoly(challengeFiles) {
|
||||
// TODO: figure out how challengeFiles might be null/not have .every as a
|
||||
// function
|
||||
return challengeFiles?.every(file => isPoly(file));
|
||||
}
|
||||
|
||||
function clearCodeEpic(action$, state$) {
|
||||
@ -79,13 +84,15 @@ function saveCodeEpic(action$, state$) {
|
||||
map(action => {
|
||||
const state = state$.value;
|
||||
const { id } = challengeMetaSelector(state);
|
||||
const files = challengeFilesSelector(state);
|
||||
const challengeFiles = challengeFilesSelector(state);
|
||||
try {
|
||||
store.set(id, files);
|
||||
// Possible fileType values: indexhtml indexjs indexjsx
|
||||
// The files Object always has one of these as the first/only attribute
|
||||
const fileType = Object.keys(files)[0];
|
||||
if (store.get(id)[fileType].contents !== files[fileType].contents) {
|
||||
store.set(id, challengeFiles);
|
||||
const fileKey = challengeFiles[0].fileKey;
|
||||
if (
|
||||
store.get(id).find(challengeFile => challengeFile.fileKey === fileKey)
|
||||
.contents !==
|
||||
challengeFiles.find(challengeFile => challengeFile.fileKey).contents
|
||||
) {
|
||||
throw Error('Failed to save to localStorage');
|
||||
}
|
||||
return action;
|
||||
@ -112,46 +119,37 @@ function loadCodeEpic(action$, state$) {
|
||||
return action$.pipe(
|
||||
ofType(actionTypes.challengeMounted),
|
||||
filter(() => {
|
||||
const files = challengeFilesSelector(state$.value);
|
||||
return Object.keys(files).length > 0;
|
||||
const challengeFiles = challengeFilesSelector(state$.value);
|
||||
return challengeFiles?.length > 0;
|
||||
}),
|
||||
switchMap(({ payload: id }) => {
|
||||
let finalFiles;
|
||||
const state = state$.value;
|
||||
const challenge = challengeMetaSelector(state);
|
||||
const files = challengeFilesSelector(state);
|
||||
const fileKeys = Object.keys(files);
|
||||
const challengeFiles = challengeFilesSelector(state);
|
||||
const fileKeys = challengeFiles.map(x => x.fileKey);
|
||||
const invalidForLegacy = fileKeys.length > 1;
|
||||
const { title: legacyKey } = challenge;
|
||||
|
||||
const codeFound = getCode(id);
|
||||
if (codeFound && isFilesAllPoly(codeFound)) {
|
||||
finalFiles = {
|
||||
...fileKeys
|
||||
.map(key => files[key])
|
||||
.reduce(
|
||||
(files, file) => ({
|
||||
...files,
|
||||
[file.key]: {
|
||||
...file,
|
||||
contents: codeFound[file.key]
|
||||
? codeFound[file.key].contents
|
||||
: file.contents,
|
||||
editableContents: codeFound[file.key]
|
||||
? codeFound[file.key].editableContents
|
||||
: file.editableContents,
|
||||
editableRegionBoundaries: codeFound[file.key]
|
||||
? codeFound[file.key].editableRegionBoundaries
|
||||
: file.editableRegionBoundaries
|
||||
finalFiles = challengeFiles.reduce((challengeFiles, challengeFile) => {
|
||||
const foundChallengeFile = codeFound.find(
|
||||
x => x.fileKey === challengeFile.fileKey
|
||||
);
|
||||
const isCodeFound = Object.keys(foundChallengeFile).length > 0;
|
||||
return [
|
||||
...challengeFiles,
|
||||
{
|
||||
...challengeFile,
|
||||
...(isCodeFound ? foundChallengeFile : {})
|
||||
}
|
||||
}),
|
||||
{}
|
||||
)
|
||||
};
|
||||
];
|
||||
}, []);
|
||||
} else {
|
||||
const legacyCode = getLegacyCode(legacyKey);
|
||||
if (legacyCode && !invalidForLegacy) {
|
||||
finalFiles = legacyToFile(legacyCode, files, fileKeys[0]);
|
||||
finalFiles = legacyToFile(legacyCode, challengeFiles, fileKeys[0]);
|
||||
}
|
||||
}
|
||||
if (finalFiles) {
|
||||
|
@ -63,11 +63,11 @@ function submitModern(type, state) {
|
||||
|
||||
if (type === actionTypes.submitChallenge) {
|
||||
const { id } = challengeMetaSelector(state);
|
||||
const files = challengeFilesSelector(state);
|
||||
const challengeFiles = challengeFilesSelector(state);
|
||||
const { username } = userSelector(state);
|
||||
const challengeInfo = {
|
||||
id,
|
||||
files
|
||||
challengeFiles
|
||||
};
|
||||
const update = {
|
||||
endpoint: '/modern-challenge-completed',
|
||||
|
@ -14,16 +14,17 @@ import { actionTypes } from './action-types';
|
||||
|
||||
const { forumLocation } = envData;
|
||||
|
||||
function filesToMarkdown(files = {}) {
|
||||
const moreThenOneFile = Object.keys(files).length > 1;
|
||||
return Object.keys(files).reduce((fileString, key) => {
|
||||
const file = files[key];
|
||||
if (!file) {
|
||||
function filesToMarkdown(challengeFiles = {}) {
|
||||
const moreThanOneFile = challengeFiles?.length > 1;
|
||||
return challengeFiles.reduce((fileString, challengeFile) => {
|
||||
if (!challengeFile) {
|
||||
return fileString;
|
||||
}
|
||||
const fileName = moreThenOneFile ? `\\ file: ${file.contents}` : '';
|
||||
const fileType = file.ext;
|
||||
return `${fileString}\`\`\`${fileType}\n${fileName}\n${file.contents}\n\`\`\`\n\n`;
|
||||
const fileName = moreThanOneFile
|
||||
? `\\ file: ${challengeFile.contents}`
|
||||
: '';
|
||||
const fileType = challengeFile.ext;
|
||||
return `${fileString}\`\`\`${fileType}\n${fileName}\n${challengeFile.contents}\n\`\`\`\n\n`;
|
||||
}, '\n');
|
||||
}
|
||||
|
||||
@ -32,7 +33,7 @@ function createQuestionEpic(action$, state$, { window }) {
|
||||
ofType(actionTypes.createQuestion),
|
||||
tap(() => {
|
||||
const state = state$.value;
|
||||
const files = challengeFilesSelector(state);
|
||||
const challengeFiles = challengeFilesSelector(state);
|
||||
const { title: challengeTitle, helpCategory } =
|
||||
challengeMetaSelector(state);
|
||||
const {
|
||||
@ -64,7 +65,7 @@ function createQuestionEpic(action$, state$, { window }) {
|
||||
${
|
||||
projectFormValues
|
||||
?.map(([key, val]) => `${key}: ${transformEditorLink(val)}\n`)
|
||||
?.join('') || filesToMarkdown(files)
|
||||
?.join('') || filesToMarkdown(challengeFiles)
|
||||
}\n\n
|
||||
${endingText}`);
|
||||
|
||||
|
@ -19,7 +19,7 @@ export { ns };
|
||||
const initialState = {
|
||||
canFocusEditor: true,
|
||||
visibleEditors: {},
|
||||
challengeFiles: {},
|
||||
challengeFiles: [],
|
||||
challengeMeta: {
|
||||
superBlock: '',
|
||||
block: '',
|
||||
@ -60,24 +60,21 @@ export const sagas = [
|
||||
export const createFiles = createAction(
|
||||
actionTypes.createFiles,
|
||||
challengeFiles =>
|
||||
Object.keys(challengeFiles)
|
||||
.filter(key => challengeFiles[key])
|
||||
.map(key => challengeFiles[key])
|
||||
.reduce(
|
||||
(challengeFiles, file) => ({
|
||||
challengeFiles.reduce((challengeFiles, challengeFile) => {
|
||||
return [
|
||||
...challengeFiles,
|
||||
[file.key]: {
|
||||
...createPoly(file),
|
||||
seed: file.contents.slice(),
|
||||
{
|
||||
...createPoly(challengeFile),
|
||||
seed: challengeFile.contents.slice(),
|
||||
editableContents: getLines(
|
||||
file.contents,
|
||||
file.editableRegionBoundaries
|
||||
challengeFile.contents,
|
||||
challengeFile.editableRegionBoundaries
|
||||
),
|
||||
seedEditableRegionBoundaries: file.editableRegionBoundaries.slice()
|
||||
seedEditableRegionBoundaries:
|
||||
challengeFile.editableRegionBoundaries.slice()
|
||||
}
|
||||
}),
|
||||
{}
|
||||
)
|
||||
];
|
||||
}, [])
|
||||
);
|
||||
|
||||
export const createQuestion = createAction(actionTypes.createQuestion);
|
||||
@ -165,7 +162,7 @@ export const challengeDataSelector = state => {
|
||||
) {
|
||||
challengeData = {
|
||||
...challengeData,
|
||||
files: challengeFilesSelector(state)
|
||||
challengeFiles: challengeFilesSelector(state)
|
||||
};
|
||||
} else if (challengeType === challengeTypes.backend) {
|
||||
const { solution: url = {} } = projectFormValuesSelector(state);
|
||||
@ -196,7 +193,7 @@ export const challengeDataSelector = state => {
|
||||
const { required = [], template = '' } = challengeMetaSelector(state);
|
||||
challengeData = {
|
||||
...challengeData,
|
||||
files: challengeFilesSelector(state),
|
||||
challengeFiles: challengeFilesSelector(state),
|
||||
required,
|
||||
template
|
||||
};
|
||||
@ -216,19 +213,21 @@ export const reducer = handleActions(
|
||||
}),
|
||||
[actionTypes.updateFile]: (
|
||||
state,
|
||||
{ payload: { key, editorValue, editableRegionBoundaries } }
|
||||
) => ({
|
||||
{ payload: { fileKey, editorValue, editableRegionBoundaries } }
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
challengeFiles: {
|
||||
...state.challengeFiles,
|
||||
[key]: {
|
||||
...state.challengeFiles[key],
|
||||
challengeFiles: [
|
||||
...state.challengeFiles.filter(x => x.fileKey !== fileKey),
|
||||
{
|
||||
...state.challengeFiles.find(x => x.fileKey === fileKey),
|
||||
contents: editorValue,
|
||||
editableContents: getLines(editorValue, editableRegionBoundaries),
|
||||
editableRegionBoundaries
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
};
|
||||
},
|
||||
[actionTypes.storedCodeFound]: (state, { payload }) => ({
|
||||
...state,
|
||||
challengeFiles: payload
|
||||
@ -268,35 +267,33 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
challengeMeta: { ...payload }
|
||||
}),
|
||||
|
||||
[actionTypes.resetChallenge]: state => ({
|
||||
...state,
|
||||
currentTab: 2,
|
||||
challengeFiles: {
|
||||
...Object.keys(state.challengeFiles)
|
||||
.map(key => state.challengeFiles[key])
|
||||
.reduce(
|
||||
(files, file) => ({
|
||||
...files,
|
||||
[file.key]: {
|
||||
...file,
|
||||
contents: file.seed.slice(),
|
||||
[actionTypes.resetChallenge]: state => {
|
||||
const challengeFilesReset = [
|
||||
...state.challengeFiles.reduce(
|
||||
(challengeFiles, challengeFile) => ({
|
||||
...challengeFiles,
|
||||
...challengeFile,
|
||||
contents: challengeFile.seed.slice(),
|
||||
editableContents: getLines(
|
||||
file.seed,
|
||||
file.seedEditableRegionBoundaries
|
||||
challengeFile.seed,
|
||||
challengeFile.seedEditableRegionBoundaries
|
||||
),
|
||||
editableRegionBoundaries: file.seedEditableRegionBoundaries
|
||||
}
|
||||
editableRegionBoundaries: challengeFile.seedEditableRegionBoundaries
|
||||
}),
|
||||
{}
|
||||
)
|
||||
},
|
||||
];
|
||||
return {
|
||||
...state,
|
||||
currentTab: 2,
|
||||
challengeFiles: challengeFilesReset,
|
||||
challengeTests: state.challengeTests.map(({ text, testString }) => ({
|
||||
text,
|
||||
testString
|
||||
})),
|
||||
consoleOut: []
|
||||
}),
|
||||
};
|
||||
},
|
||||
[actionTypes.updateSolutionFormValues]: (state, { payload }) => ({
|
||||
...state,
|
||||
projectFormValues: payload
|
||||
|
@ -49,7 +49,7 @@ const applyFunction = fn =>
|
||||
const composeFunctions = (...fns) =>
|
||||
fns.map(applyFunction).reduce((f, g) => x => f(x).then(g));
|
||||
|
||||
function buildSourceMap(files) {
|
||||
function buildSourceMap(challengeFiles) {
|
||||
// TODO: concatenating the source/contents is a quick hack for multi-file
|
||||
// editing. It is used because all the files (js, html and css) end up with
|
||||
// the same name 'index'. This made the last file the only file to appear in
|
||||
@ -57,22 +57,26 @@ function buildSourceMap(files) {
|
||||
// A better solution is to store and handle them separately. Perhaps never
|
||||
// setting the name to 'index'. Use 'contents' instead?
|
||||
// TODO: is file.source ever defined?
|
||||
return files.reduce(
|
||||
(sources, file) => {
|
||||
sources[file.name] += file.source || file.contents;
|
||||
sources.editableContents += file.editableContents || '';
|
||||
const source = challengeFiles.reduce(
|
||||
(sources, challengeFile) => {
|
||||
sources[challengeFile.name] +=
|
||||
challengeFile.source || challengeFile.contents;
|
||||
sources.editableContents += challengeFile.editableContents || '';
|
||||
return sources;
|
||||
},
|
||||
{ index: '', editableContents: '' }
|
||||
);
|
||||
return source;
|
||||
}
|
||||
|
||||
function checkFilesErrors(files) {
|
||||
const errors = files.filter(({ error }) => error).map(({ error }) => error);
|
||||
function checkFilesErrors(challengeFiles) {
|
||||
const errors = challengeFiles
|
||||
.filter(({ error }) => error)
|
||||
.map(({ error }) => error);
|
||||
if (errors.length) {
|
||||
throw errors;
|
||||
}
|
||||
return files;
|
||||
return challengeFiles;
|
||||
}
|
||||
|
||||
const buildFunctions = {
|
||||
@ -140,41 +144,48 @@ async function getDOMTestRunner(buildData, { proxyLogger }, document) {
|
||||
runTestInTestFrame(document, testString, testTimeout);
|
||||
}
|
||||
|
||||
export function buildDOMChallenge({ files, required = [], template = '' }) {
|
||||
export function buildDOMChallenge({
|
||||
challengeFiles,
|
||||
required = [],
|
||||
template = ''
|
||||
}) {
|
||||
const finalRequires = [...globalRequires, ...required, ...frameRunner];
|
||||
const loadEnzyme = Object.keys(files).some(key => files[key].ext === 'jsx');
|
||||
const loadEnzyme = challengeFiles.some(
|
||||
challengeFile => challengeFile.ext === 'jsx'
|
||||
);
|
||||
const toHtml = [jsToHtml, cssToHtml];
|
||||
const pipeLine = composeFunctions(...getTransformers(), ...toHtml);
|
||||
const finalFiles = Object.keys(files)
|
||||
.map(key => files[key])
|
||||
.map(pipeLine);
|
||||
const finalFiles = challengeFiles.map(pipeLine);
|
||||
return Promise.all(finalFiles)
|
||||
.then(checkFilesErrors)
|
||||
.then(files => ({
|
||||
.then(challengeFiles => ({
|
||||
challengeType: challengeTypes.html,
|
||||
build: concatHtml({ required: finalRequires, template, files }),
|
||||
sources: buildSourceMap(files),
|
||||
build: concatHtml({ required: finalRequires, template, challengeFiles }),
|
||||
sources: buildSourceMap(challengeFiles),
|
||||
loadEnzyme
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildJSChallenge({ files }, options) {
|
||||
export function buildJSChallenge({ challengeFiles }, options) {
|
||||
const pipeLine = composeFunctions(...getTransformers(options));
|
||||
|
||||
const finalFiles = Object.keys(files)
|
||||
.map(key => files[key])
|
||||
.map(pipeLine);
|
||||
const finalFiles = challengeFiles.map(pipeLine);
|
||||
return Promise.all(finalFiles)
|
||||
.then(checkFilesErrors)
|
||||
.then(files => ({
|
||||
.then(challengeFiles => ({
|
||||
challengeType: challengeTypes.js,
|
||||
build: files
|
||||
build: challengeFiles
|
||||
.reduce(
|
||||
(body, file) => [...body, file.head, file.contents, file.tail],
|
||||
(body, challengeFile) => [
|
||||
...body,
|
||||
challengeFile.head,
|
||||
challengeFile.contents,
|
||||
challengeFile.tail
|
||||
],
|
||||
[]
|
||||
)
|
||||
.join('\n'),
|
||||
sources: buildSourceMap(files)
|
||||
sources: buildSourceMap(challengeFiles)
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,11 @@ import { toSortedArray } from '../../../../../utils/sort-files';
|
||||
export function getTargetEditor(challengeFiles) {
|
||||
if (isEmpty(challengeFiles)) return null;
|
||||
else {
|
||||
let targetEditor = Object.values(challengeFiles).find(
|
||||
let targetEditor = challengeFiles.find(
|
||||
({ editableRegionBoundaries }) => !isEmpty(editableRegionBoundaries)
|
||||
)?.key;
|
||||
)?.fileKey;
|
||||
|
||||
// fallback for when there is no editable region.
|
||||
return targetEditor || toSortedArray(challengeFiles)[0].key;
|
||||
return targetEditor || toSortedArray(challengeFiles)[0].fileKey;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
const yaml = require('js-yaml');
|
||||
const { findIndex, reduce, toString } = require('lodash');
|
||||
const { findIndex } = require('lodash');
|
||||
const readDirP = require('readdirp');
|
||||
const { helpCategoryMap } = require('../client/utils/challenge-types');
|
||||
const { showUpcomingChanges } = require('../config/env.json');
|
||||
@ -306,45 +306,22 @@ ${getFullPath('english')}
|
||||
return prepareChallenge(challenge);
|
||||
}
|
||||
|
||||
// TODO: tests and more descriptive name.
|
||||
function filesToObject(files) {
|
||||
return reduce(
|
||||
files,
|
||||
(map, file) => {
|
||||
map[file.key] = {
|
||||
...file,
|
||||
head: arrToString(file.head),
|
||||
contents: arrToString(file.contents),
|
||||
tail: arrToString(file.tail)
|
||||
};
|
||||
return map;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// gets the challenge ready for sourcing into Gatsby
|
||||
function prepareChallenge(challenge) {
|
||||
if (challenge.files) {
|
||||
challenge.files = filesToObject(challenge.files);
|
||||
challenge.files = Object.keys(challenge.files)
|
||||
.filter(key => challenge.files[key])
|
||||
.map(key => challenge.files[key])
|
||||
.reduce(
|
||||
(files, file) => ({
|
||||
...files,
|
||||
[file.key]: {
|
||||
...createPoly(file),
|
||||
seed: file.contents.slice(0)
|
||||
if (challenge.challengeFiles) {
|
||||
challenge.challengeFiles = challenge.challengeFiles.reduce(
|
||||
(challengeFiles, challengeFile) => {
|
||||
return [
|
||||
...challengeFiles,
|
||||
{
|
||||
...createPoly(challengeFile),
|
||||
seed: challengeFile.contents.slice(0)
|
||||
}
|
||||
}),
|
||||
{}
|
||||
];
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
if (challenge.solutionFiles) {
|
||||
challenge.solutionFiles = filesToObject(challenge.solutionFiles);
|
||||
}
|
||||
return challenge;
|
||||
}
|
||||
|
||||
@ -381,10 +358,6 @@ function getBlockNameFromPath(filePath) {
|
||||
return block;
|
||||
}
|
||||
|
||||
function arrToString(arr) {
|
||||
return Array.isArray(arr) ? arr.join('\n') : toString(arr);
|
||||
}
|
||||
|
||||
exports.hasEnglishSource = hasEnglishSource;
|
||||
exports.parseTranslation = parseTranslation;
|
||||
exports.createChallenge = createChallenge;
|
||||
|
@ -6,7 +6,7 @@ const { challengeTypes } = require('../../client/utils/challenge-types');
|
||||
const slugRE = new RegExp('^[a-z0-9-]+$');
|
||||
|
||||
const fileJoi = Joi.object().keys({
|
||||
key: Joi.string(),
|
||||
fileKey: Joi.string(),
|
||||
ext: Joi.string(),
|
||||
name: Joi.string(),
|
||||
editableRegionBoundaries: [Joi.array().items(Joi.number())],
|
||||
@ -37,12 +37,7 @@ const schema = Joi.object()
|
||||
then: Joi.string().allow(''),
|
||||
otherwise: Joi.string().required()
|
||||
}),
|
||||
files: Joi.object().keys({
|
||||
indexcss: fileJoi,
|
||||
indexhtml: fileJoi,
|
||||
indexjs: fileJoi,
|
||||
indexjsx: fileJoi
|
||||
}),
|
||||
challengeFiles: Joi.array().items(fileJoi),
|
||||
guideUrl: Joi.string().uri({ scheme: 'https' }),
|
||||
helpCategory: Joi.valid(
|
||||
'JavaScript',
|
||||
@ -76,15 +71,7 @@ const schema = Joi.object()
|
||||
crossDomain: Joi.bool()
|
||||
})
|
||||
),
|
||||
solutions: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
indexcss: fileJoi,
|
||||
indexhtml: fileJoi,
|
||||
indexjs: fileJoi,
|
||||
indexjsx: fileJoi,
|
||||
indexpy: fileJoi
|
||||
})
|
||||
),
|
||||
solutions: Joi.array().items(Joi.array().items(fileJoi)),
|
||||
superBlock: Joi.string().regex(slugRE),
|
||||
superOrder: Joi.number(),
|
||||
suborder: Joi.number(),
|
||||
|
@ -307,16 +307,16 @@ function populateTestsForLang({ lang, challenges, meta }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If no .files, then no seed:
|
||||
if (!challenge.files) return;
|
||||
// If no .challengeFiles, then no seed:
|
||||
if (!challenge.challengeFiles) return;
|
||||
|
||||
// - None of the translatable comments should appear in the
|
||||
// translations. While this is a crude check, no challenges
|
||||
// currently have the text of a comment elsewhere. If that happens
|
||||
// we can handle that challenge separately.
|
||||
TRANSLATABLE_COMMENTS.forEach(comment => {
|
||||
Object.values(challenge.files).forEach(file => {
|
||||
if (file.contents.includes(comment))
|
||||
challenge.challengeFiles.forEach(challengeFile => {
|
||||
if (challengeFile.contents.includes(comment))
|
||||
throw Error(
|
||||
`English comment '${comment}' should be replaced with its translation`
|
||||
);
|
||||
@ -325,14 +325,16 @@ function populateTestsForLang({ lang, challenges, meta }) {
|
||||
|
||||
// - None of the translated comment texts should appear *outside* a
|
||||
// comment
|
||||
Object.values(challenge.files).forEach(file => {
|
||||
challenge.challengeFiles.forEach(challengeFile => {
|
||||
let comments = {};
|
||||
|
||||
// We get all the actual comments using the appropriate parsers
|
||||
if (file.ext === 'html') {
|
||||
if (challengeFile.ext === 'html') {
|
||||
const commentTypes = ['css', 'html', 'scriptJs'];
|
||||
for (let type of commentTypes) {
|
||||
const newComments = commentExtractors[type](file.contents);
|
||||
const newComments = commentExtractors[type](
|
||||
challengeFile.contents
|
||||
);
|
||||
for (const [key, value] of Object.entries(newComments)) {
|
||||
comments[key] = comments[key]
|
||||
? comments[key] + value
|
||||
@ -340,7 +342,9 @@ function populateTestsForLang({ lang, challenges, meta }) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
comments = commentExtractors[file.ext](file.contents);
|
||||
comments = commentExtractors[challengeFile.ext](
|
||||
challengeFile.contents
|
||||
);
|
||||
}
|
||||
|
||||
// Then we compare the number of times each comment appears in the
|
||||
@ -409,7 +413,7 @@ ${inspect(commentMap)}
|
||||
try {
|
||||
testRunner = await createTestRunner(
|
||||
challenge,
|
||||
'',
|
||||
[],
|
||||
buildChallenge
|
||||
);
|
||||
} catch {
|
||||
@ -448,12 +452,13 @@ ${inspect(commentMap)}
|
||||
// TODO: can this be dried out, ideally by removing the redux
|
||||
// handler?
|
||||
if (nextChallenge) {
|
||||
const solutionFiles = cloneDeep(nextChallenge.files);
|
||||
Object.keys(solutionFiles).forEach(key => {
|
||||
const file = solutionFiles[key];
|
||||
file.editableContents = getLines(
|
||||
file.contents,
|
||||
challenge.files[key].editableRegionBoundaries
|
||||
const solutionFiles = cloneDeep(nextChallenge.challengeFiles);
|
||||
solutionFiles.forEach(challengeFile => {
|
||||
challengeFile.editableContents = getLines(
|
||||
challengeFile.contents,
|
||||
challenge.challengeFiles.find(
|
||||
x => x.fileKey === challengeFile.fileKey
|
||||
).editableRegionBoundaries
|
||||
);
|
||||
});
|
||||
solutions = [solutionFiles];
|
||||
@ -470,7 +475,9 @@ ${inspect(commentMap)}
|
||||
|
||||
const filteredSolutions = solutionsAsArrays.filter(solution => {
|
||||
return !isEmpty(
|
||||
solution.filter(file => !noSolution.test(file.contents))
|
||||
solution.filter(
|
||||
challengeFile => !noSolution.test(challengeFile.contents)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@ -505,21 +512,23 @@ ${inspect(commentMap)}
|
||||
|
||||
async function createTestRunner(
|
||||
challenge,
|
||||
solution,
|
||||
solutionFiles,
|
||||
buildChallenge,
|
||||
solutionFromNext
|
||||
) {
|
||||
const { required = [], template, removeComments } = challenge;
|
||||
// we should avoid modifying challenge, as it gets reused:
|
||||
const files = cloneDeep(challenge.files);
|
||||
|
||||
Object.keys(solution).forEach(key => {
|
||||
files[key].contents = solution[key].contents;
|
||||
files[key].editableContents = solution[key].editableContents;
|
||||
const challengeFiles = cloneDeep(challenge.challengeFiles);
|
||||
solutionFiles.forEach(solutionFile => {
|
||||
const challengeFile = challengeFiles.find(
|
||||
x => x.fileKey === solutionFile.fileKey
|
||||
);
|
||||
challengeFile.contents = solutionFile.contents;
|
||||
challengeFile.editableContents = solutionFile.editableContents;
|
||||
});
|
||||
|
||||
const { build, sources, loadEnzyme } = await buildChallenge({
|
||||
files,
|
||||
challengeFiles,
|
||||
required,
|
||||
template
|
||||
});
|
||||
|
37
cypress/integration/learn/challenges/code-storage.js
Normal file
37
cypress/integration/learn/challenges/code-storage.js
Normal file
@ -0,0 +1,37 @@
|
||||
/* global cy */
|
||||
|
||||
const selectors = {
|
||||
defaultOutput: '.output-text',
|
||||
editor: '.react-monaco-editor-container'
|
||||
};
|
||||
|
||||
const location =
|
||||
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements';
|
||||
|
||||
describe('Challenge with editor', function () {
|
||||
before(() => {
|
||||
cy.visit(location);
|
||||
});
|
||||
|
||||
it('renders seed code without localStorage', () => {
|
||||
const editorContents = `<h1>Hello</h1>`;
|
||||
cy.get(selectors.editor).as('editor').contains(editorContents);
|
||||
cy.get('@editor').click().focused().type(`{movetoend}<h1>Hello World</h1>`);
|
||||
cy.reload();
|
||||
cy.get('@editor', { timeout: 10000 }).contains(editorContents);
|
||||
});
|
||||
|
||||
it('renders code from localStorage after "Ctrl + S"', () => {
|
||||
const editorContents = `<h1>Hello</h1>`;
|
||||
cy.get(selectors.editor).as('editor').contains(editorContents);
|
||||
cy.get('@editor')
|
||||
.click()
|
||||
.focused()
|
||||
.type(`{movetoend}<h1>Hello World</h1>{ctrl+s}`);
|
||||
cy.contains("Saved! Your code was saved to your browser's local storage.");
|
||||
cy.reload();
|
||||
cy.get('@editor', { timeout: 10000 }).contains(
|
||||
'<h1>Hello</h1><h1>Hello World</h1>'
|
||||
);
|
||||
});
|
||||
});
|
@ -13,7 +13,7 @@ a container directive
|
||||
:::</p>
|
||||
</section>",
|
||||
"solutions": Array [
|
||||
Object {},
|
||||
Array [],
|
||||
],
|
||||
"tests": Array [],
|
||||
}
|
||||
@ -44,7 +44,7 @@ Object {
|
||||
</code></pre>",
|
||||
},
|
||||
"solutions": Array [
|
||||
Object {},
|
||||
Array [],
|
||||
],
|
||||
"tests": Array [],
|
||||
}
|
||||
@ -52,38 +52,33 @@ Object {
|
||||
|
||||
exports[`challenge parser should import md from other files 1`] = `
|
||||
Object {
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>Paragraph 1</p>
|
||||
<pre><code class=\\"language-html\\">code example
|
||||
</code></pre>
|
||||
</section>",
|
||||
"files": Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"challengeFiles": Array [
|
||||
Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
const element = array[index];
|
||||
@ -91,20 +86,25 @@ for (let index = 0; index < array.length; index++) {
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "custom-name",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>Paragraph 1</p>
|
||||
<pre><code class=\\"language-html\\">code example
|
||||
</code></pre>
|
||||
</section>",
|
||||
"instructions": "<section id=\\"instructions\\">
|
||||
<p>Paragraph 0</p>
|
||||
<pre><code class=\\"language-html\\">code example 0
|
||||
</code></pre>
|
||||
</section>",
|
||||
"solutions": Array [
|
||||
Object {},
|
||||
Array [],
|
||||
],
|
||||
"tests": Array [
|
||||
Object {
|
||||
@ -121,6 +121,43 @@ for (let index = 0; index < array.length; index++) {
|
||||
|
||||
exports[`challenge parser should not mix other YAML with the frontmatter 1`] = `
|
||||
Object {
|
||||
"challengeFiles": Array [
|
||||
Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
],
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>Paragraph 1</p>
|
||||
<pre><code class=\\"language-html\\">code example
|
||||
@ -130,50 +167,13 @@ Object {
|
||||
anothersubkey: another value
|
||||
</code></pre>
|
||||
</section>",
|
||||
"files": Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "html",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
"instructions": "<section id=\\"instructions\\">
|
||||
<p>Paragraph 0</p>
|
||||
<pre><code class=\\"language-html\\">code example 0
|
||||
</code></pre>
|
||||
</section>",
|
||||
"solutions": Array [
|
||||
Object {},
|
||||
Array [],
|
||||
],
|
||||
"tests": Array [
|
||||
Object {
|
||||
@ -190,42 +190,8 @@ Object {
|
||||
|
||||
exports[`challenge parser should parse a more realistic md file 1`] = `
|
||||
Object {
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>When you add a lower rank heading element to the page, it's implied that you're starting a new subsection.</p>
|
||||
<p>After the last <code>h2</code> element of the second <code>section</code> element, add an <code>h3</code> element with the text <code>Things cats love:</code>.</p>
|
||||
<blockquote>
|
||||
<p>Some text in a blockquote</p>
|
||||
<p>
|
||||
Some text in a blockquote, with <code>code</code>
|
||||
</p>
|
||||
</blockquote>
|
||||
</section>",
|
||||
"files": Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [
|
||||
7,
|
||||
9,
|
||||
],
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"challengeFiles": Array [
|
||||
Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
<h1>CatPhotoApp</h1>
|
||||
@ -256,31 +222,13 @@ a {
|
||||
23,
|
||||
],
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "html-key",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"head": " // this runs before the user's code is evaluated.",
|
||||
"id": "final-key",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
"instructions": "<section id=\\"instructions\\">
|
||||
<p>Do something with the <code>code</code>.</p>
|
||||
<p>To test that adjacent tags are handled correctly:</p>
|
||||
<p>a bit of <code>code</code> <tag>with more after a space</tag> and another pair of <strong>elements</strong> <em>with a space</em></p>
|
||||
</section>",
|
||||
"solutions": Array [
|
||||
Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}
|
||||
@ -293,14 +241,46 @@ h1 {
|
||||
a {
|
||||
color: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [
|
||||
7,
|
||||
9,
|
||||
],
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": " // this runs before the user's code is evaluated.",
|
||||
"id": "final-key",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
],
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>When you add a lower rank heading element to the page, it's implied that you're starting a new subsection.</p>
|
||||
<p>After the last <code>h2</code> element of the second <code>section</code> element, add an <code>h3</code> element with the text <code>Things cats love:</code>.</p>
|
||||
<blockquote>
|
||||
<p>Some text in a blockquote</p>
|
||||
<p>
|
||||
Some text in a blockquote, with <code>code</code>
|
||||
</p>
|
||||
</blockquote>
|
||||
</section>",
|
||||
"instructions": "<section id=\\"instructions\\">
|
||||
<p>Do something with the <code>code</code>.</p>
|
||||
<p>To test that adjacent tags are handled correctly:</p>
|
||||
<p>a bit of <code>code</code> <tag>with more after a space</tag> and another pair of <strong>elements</strong> <em>with a space</em></p>
|
||||
</section>",
|
||||
"solutions": Array [
|
||||
Array [
|
||||
Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
<h1>CatPhotoApp</h1>
|
||||
@ -327,22 +307,42 @@ a {
|
||||
</body>
|
||||
</html>",
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "html-key",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: green;
|
||||
}",
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';",
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "final-key",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"tests": Array [
|
||||
Object {
|
||||
@ -385,89 +385,89 @@ assert(
|
||||
|
||||
exports[`challenge parser should parse a simple md file 1`] = `
|
||||
Object {
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>Paragraph 1</p>
|
||||
<pre><code class=\\"language-html\\">code example
|
||||
</code></pre>
|
||||
</section>",
|
||||
"files": Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"challengeFiles": Array [
|
||||
Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>Paragraph 1</p>
|
||||
<pre><code class=\\"language-html\\">code example
|
||||
</code></pre>
|
||||
</section>",
|
||||
"instructions": "<section id=\\"instructions\\">
|
||||
<p>Paragraph 0</p>
|
||||
<pre><code class=\\"language-html\\">code example 0
|
||||
</code></pre>
|
||||
</section>",
|
||||
"solutions": Array [
|
||||
Array [
|
||||
Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}",
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "html-key",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}",
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';
|
||||
\`\`",
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"tests": Array [
|
||||
Object {
|
||||
@ -491,54 +491,54 @@ if(let x of xs) {
|
||||
|
||||
exports[`challenge parser should parse frontmatter 1`] = `
|
||||
Object {
|
||||
"challengeType": 0,
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>Paragraph 1</p>
|
||||
<pre><code class=\\"language-html\\">code example
|
||||
</code></pre>
|
||||
</section>",
|
||||
"files": Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"challengeFiles": Array [
|
||||
Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
"challengeType": 0,
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>Paragraph 1</p>
|
||||
<pre><code class=\\"language-html\\">code example
|
||||
</code></pre>
|
||||
</section>",
|
||||
"forumTopicId": 18276,
|
||||
"id": "bd7123c8c441eddfaeb5bdef",
|
||||
"isHidden": false,
|
||||
"solutions": Array [
|
||||
Object {},
|
||||
Array [],
|
||||
],
|
||||
"tests": Array [
|
||||
Object {
|
||||
@ -557,6 +557,43 @@ Object {
|
||||
|
||||
exports[`challenge parser should parse gfm strikethrough and frontmatter 1`] = `
|
||||
Object {
|
||||
"challengeFiles": Array [
|
||||
Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
],
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>Paragraph 1 <del>Strikethrough text</del>. https://should.not.be.autolinked</p>
|
||||
<pre><code class=\\"language-html\\">code example
|
||||
@ -576,84 +613,47 @@ Object {
|
||||
</tbody>
|
||||
</table>
|
||||
</section>",
|
||||
"files": Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "html",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
"instructions": "<section id=\\"instructions\\">
|
||||
<p>Paragraph 0</p>
|
||||
<pre><code class=\\"language-html\\">code example 0
|
||||
</code></pre>
|
||||
</section>",
|
||||
"solutions": Array [
|
||||
Array [
|
||||
Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}",
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "html-key",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}",
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';
|
||||
\`\`",
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"tests": Array [
|
||||
Object {
|
||||
|
@ -2,42 +2,42 @@
|
||||
|
||||
exports[`add-seed plugin should have an output to match the snapshot 1`] = `
|
||||
Object {
|
||||
"files": Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"challengeFiles": Array [
|
||||
Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
@ -3,41 +3,41 @@
|
||||
exports[`add solution plugin should have an output to match the snapshot 1`] = `
|
||||
Object {
|
||||
"solutions": Array [
|
||||
Array [
|
||||
Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}",
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"ext": "html",
|
||||
"fileKey": "indexhtml",
|
||||
"head": "",
|
||||
"id": "html-key",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}",
|
||||
"ext": "css",
|
||||
"fileKey": "indexcss",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
Object {
|
||||
"contents": "var x = 'y';
|
||||
\`\`",
|
||||
"ext": "js",
|
||||
"fileKey": "indexjs",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
@ -7,8 +7,8 @@ const { getFileVisitor } = require('./utils/get-file-visitor');
|
||||
|
||||
const editableRegionMarker = '--fcc-editable-region--';
|
||||
|
||||
function findRegionMarkers(file) {
|
||||
const lines = file.contents.split('\n');
|
||||
function findRegionMarkers(challengeFile) {
|
||||
const lines = challengeFile.contents.split('\n');
|
||||
const editableLines = lines
|
||||
.map((line, id) => (line.trim() === editableRegionMarker ? id : -1))
|
||||
.filter(id => id >= 0);
|
||||
@ -55,26 +55,33 @@ function addSeeds() {
|
||||
visitForContents(contentsTree);
|
||||
visitForHead(headTree);
|
||||
visitForTail(tailTree);
|
||||
const seedVals = Object.values(seeds);
|
||||
file.data = {
|
||||
...file.data,
|
||||
files: seeds
|
||||
challengeFiles: seedVals
|
||||
};
|
||||
|
||||
// process region markers - remove them from content and add them to data
|
||||
Object.keys(seeds).forEach(key => {
|
||||
const fileData = seeds[key];
|
||||
const editRegionMarkers = findRegionMarkers(fileData);
|
||||
const challengeFiles = Object.values(seeds).map(data => {
|
||||
const seed = { ...data };
|
||||
const editRegionMarkers = findRegionMarkers(seed);
|
||||
if (editRegionMarkers) {
|
||||
fileData.contents = removeLines(fileData.contents, editRegionMarkers);
|
||||
seed.contents = removeLines(seed.contents, editRegionMarkers);
|
||||
|
||||
if (editRegionMarkers[1] <= editRegionMarkers[0]) {
|
||||
throw Error('Editable region must be non zero');
|
||||
}
|
||||
fileData.editableRegionBoundaries = editRegionMarkers;
|
||||
seed.editableRegionBoundaries = editRegionMarkers;
|
||||
} else {
|
||||
fileData.editableRegionBoundaries = [];
|
||||
seed.editableRegionBoundaries = [];
|
||||
}
|
||||
return seed;
|
||||
});
|
||||
|
||||
file.data = {
|
||||
...file.data,
|
||||
challengeFiles
|
||||
};
|
||||
}
|
||||
|
||||
return transformer;
|
||||
|
@ -1,4 +1,3 @@
|
||||
const { isObject } = require('lodash');
|
||||
const isArray = require('lodash/isArray');
|
||||
|
||||
const adjacentKeysAST = require('../__fixtures__/ast-adjacent-keys.json');
|
||||
@ -32,26 +31,26 @@ describe('add-seed plugin', () => {
|
||||
expect(typeof plugin).toEqual('function');
|
||||
});
|
||||
|
||||
it('adds a `files` property to `file.data`', () => {
|
||||
it('adds a `challengeFiles` property to `file.data`', () => {
|
||||
plugin(simpleAST, file);
|
||||
expect('files' in file.data).toBe(true);
|
||||
expect('challengeFiles' in file.data).toBe(true);
|
||||
});
|
||||
|
||||
it('ensures that the `files` property is an object', () => {
|
||||
it('ensures that the `challengeFiles` property is an array', () => {
|
||||
plugin(simpleAST, file);
|
||||
expect(isObject(file.data.files)).toBe(true);
|
||||
expect(isArray(file.data.challengeFiles)).toBe(true);
|
||||
});
|
||||
|
||||
it('adds test objects to the files array following a schema', () => {
|
||||
it('adds test objects to the challengeFiles array following a schema', () => {
|
||||
expect.assertions(17);
|
||||
plugin(simpleAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
data: { challengeFiles }
|
||||
} = file;
|
||||
const testObject = files.indexjs;
|
||||
const testObject = challengeFiles.find(x => x.fileKey === 'indexjs');
|
||||
expect(Object.keys(testObject).length).toEqual(8);
|
||||
expect(testObject).toHaveProperty('key');
|
||||
expect(typeof testObject['key']).toBe('string');
|
||||
expect(testObject).toHaveProperty('fileKey');
|
||||
expect(typeof testObject['fileKey']).toBe('string');
|
||||
expect(testObject).toHaveProperty('ext');
|
||||
expect(typeof testObject['ext']).toBe('string');
|
||||
expect(testObject).toHaveProperty('name');
|
||||
@ -69,33 +68,32 @@ describe('add-seed plugin', () => {
|
||||
});
|
||||
|
||||
it('parses seeds without ids', () => {
|
||||
expect.assertions(6);
|
||||
expect.assertions(3);
|
||||
plugin(simpleAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
data: { challengeFiles }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs');
|
||||
const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml');
|
||||
const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss');
|
||||
|
||||
expect(indexjs.contents).toBe(`var x = 'y';`);
|
||||
expect(indexjs.key).toBe(`indexjs`);
|
||||
expect(indexhtml.contents).toBe(`<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>`);
|
||||
expect(indexhtml.key).toBe(`indexhtml`);
|
||||
expect(indexcss.contents).toBe(`body {
|
||||
background: green;
|
||||
}`);
|
||||
expect(indexcss.key).toBe(`indexcss`);
|
||||
});
|
||||
|
||||
it('removes region markers from contents', () => {
|
||||
expect.assertions(2);
|
||||
plugin(withEditableAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
data: { challengeFiles }
|
||||
} = file;
|
||||
const { indexcss } = files;
|
||||
const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss');
|
||||
|
||||
expect(indexcss.contents).not.toMatch('--fcc-editable-region--');
|
||||
expect(indexcss.editableRegionBoundaries).toEqual([1, 4]);
|
||||
@ -107,9 +105,11 @@ describe('add-seed plugin', () => {
|
||||
expect.assertions(3);
|
||||
plugin(withSeedKeysAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
data: { challengeFiles }
|
||||
} = file;
|
||||
const { indexhtml, indexcss, indexjs } = files;
|
||||
const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs');
|
||||
const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml');
|
||||
const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss');
|
||||
|
||||
expect(indexhtml.id).toBe('');
|
||||
expect(indexcss.id).toBe('key-for-css');
|
||||
@ -138,9 +138,11 @@ describe('add-seed plugin', () => {
|
||||
expect.assertions(3);
|
||||
plugin(withBeforeAfterAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
data: { challengeFiles }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs');
|
||||
const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml');
|
||||
const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss');
|
||||
|
||||
expect(indexjs.head).toBe('');
|
||||
expect(indexhtml.head).toBe(`<!-- comment -->`);
|
||||
@ -153,9 +155,11 @@ describe('add-seed plugin', () => {
|
||||
expect.assertions(3);
|
||||
plugin(withBeforeAfterAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
data: { challengeFiles }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs');
|
||||
const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml');
|
||||
const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss');
|
||||
|
||||
expect(indexjs.tail).toBe(`function teardown(params) {
|
||||
// after
|
||||
@ -188,9 +192,11 @@ describe('add-seed plugin', () => {
|
||||
expect.assertions(6);
|
||||
plugin(emptyBeforeAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
data: { challengeFiles }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs');
|
||||
const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml');
|
||||
const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss');
|
||||
|
||||
expect(indexjs.head).toBe('');
|
||||
expect(indexjs.tail).toBe('function teardown(params) {\n // after\n}');
|
||||
@ -204,9 +210,11 @@ describe('add-seed plugin', () => {
|
||||
expect.assertions(6);
|
||||
plugin(emptyAfterAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
data: { challengeFiles }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
const indexjs = challengeFiles.find(x => x.fileKey === 'indexjs');
|
||||
const indexhtml = challengeFiles.find(x => x.fileKey === 'indexhtml');
|
||||
const indexcss = challengeFiles.find(x => x.fileKey === 'indexcss');
|
||||
|
||||
expect(indexjs.head).toBe('');
|
||||
expect(indexjs.tail).toBe('');
|
||||
@ -234,9 +242,9 @@ describe('add-seed plugin', () => {
|
||||
expect.assertions(4);
|
||||
plugin(jsxSeedAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
data: { challengeFiles }
|
||||
} = file;
|
||||
const { indexjsx } = files;
|
||||
const indexjsx = challengeFiles.find(x => x.fileKey === 'indexjsx');
|
||||
|
||||
expect(indexjsx.head).toBe(`function setup() {}`);
|
||||
expect(indexjsx.tail).toBe(`function teardown(params) {
|
||||
@ -248,7 +256,7 @@ describe('add-seed plugin', () => {
|
||||
const Button = () => {
|
||||
return <button> {/* another comment! */} text </button>;
|
||||
};`);
|
||||
expect(indexjsx.key).toBe(`indexjsx`);
|
||||
expect(indexjsx.fileKey).toBe(`indexjsx`);
|
||||
});
|
||||
|
||||
it('combines all the code of a specific language into a single file', () => {
|
||||
|
@ -30,7 +30,7 @@ function createPlugin() {
|
||||
);
|
||||
|
||||
visitForContents(solutionTree);
|
||||
solutions.push(solution);
|
||||
solutions.push(Object.values(solution));
|
||||
});
|
||||
|
||||
file.data = {
|
||||
|
@ -33,16 +33,18 @@ describe('add solution plugin', () => {
|
||||
expect(file.data.solutions.every(el => isObject(el))).toBe(true);
|
||||
});
|
||||
|
||||
it('adds solution objects to the files array following a schema', () => {
|
||||
it('adds solution objects to the challengeFiles array following a schema', () => {
|
||||
expect.assertions(15);
|
||||
plugin(mockAST, file);
|
||||
const {
|
||||
data: { solutions }
|
||||
} = file;
|
||||
const testObject = solutions[0].indexjs;
|
||||
const testObject = solutions[0].find(
|
||||
solution => solution.fileKey === 'indexjs'
|
||||
);
|
||||
expect(Object.keys(testObject).length).toEqual(7);
|
||||
expect(testObject).toHaveProperty('key');
|
||||
expect(typeof testObject['key']).toBe('string');
|
||||
expect(testObject).toHaveProperty('fileKey');
|
||||
expect(typeof testObject['fileKey']).toBe('string');
|
||||
expect(testObject).toHaveProperty('ext');
|
||||
expect(typeof testObject['ext']).toBe('string');
|
||||
expect(testObject).toHaveProperty('name');
|
||||
@ -64,16 +66,24 @@ describe('add solution plugin', () => {
|
||||
data: { solutions }
|
||||
} = file;
|
||||
expect(solutions.length).toBe(3);
|
||||
expect(solutions[0].indexjs.contents).toBe("var x = 'y';");
|
||||
expect(solutions[1].indexhtml.contents).toBe(`<html>
|
||||
expect(
|
||||
solutions[0].find(solution => solution.fileKey === 'indexjs').contents
|
||||
).toBe("var x = 'y';");
|
||||
expect(
|
||||
solutions[1].find(solution => solution.fileKey === 'indexhtml').contents
|
||||
).toBe(`<html>
|
||||
<body>
|
||||
solution number two
|
||||
</body>
|
||||
</html>`);
|
||||
expect(solutions[1].indexcss.contents).toBe(`body {
|
||||
expect(
|
||||
solutions[1].find(solution => solution.fileKey === 'indexcss').contents
|
||||
).toBe(`body {
|
||||
background: white;
|
||||
}`);
|
||||
expect(solutions[2].indexjs.contents).toBe("var x = 'y3';");
|
||||
expect(
|
||||
solutions[2].find(solution => solution.fileKey === 'indexjs').contents
|
||||
).toBe("var x = 'y3';");
|
||||
});
|
||||
|
||||
it('should reject solutions with editable region markers', () => {
|
||||
|
@ -12,7 +12,7 @@ const supportedLanguages = ['js', 'css', 'html', 'jsx', 'py'];
|
||||
|
||||
function defaultFile(lang, id) {
|
||||
return {
|
||||
key: `index${lang}`,
|
||||
fileKey: `index${lang}`,
|
||||
ext: lang,
|
||||
name: 'index',
|
||||
contents: '',
|
||||
@ -43,21 +43,21 @@ function codeToData(node, seeds, seedKey, validate) {
|
||||
Please use one of js, css, html, jsx or py
|
||||
`);
|
||||
|
||||
const key = `index${lang}`;
|
||||
const id = seeds[key] ? seeds[key].id : '';
|
||||
const fileKey = `index${lang}`;
|
||||
const id = seeds[fileKey] ? seeds[fileKey].id : '';
|
||||
// the contents will be missing if there is an id preceding this code
|
||||
// block.
|
||||
if (!seeds[key]) {
|
||||
seeds[key] = defaultFile(lang, id);
|
||||
if (!seeds[fileKey]) {
|
||||
seeds[fileKey] = defaultFile(lang, id);
|
||||
}
|
||||
if (isEmpty(node.value) && seedKey !== 'contents') {
|
||||
const section = keyToSection[seedKey];
|
||||
throw Error(`Empty code block in --${section}-- section`);
|
||||
}
|
||||
|
||||
seeds[key][seedKey] = isEmpty(seeds[key][seedKey])
|
||||
seeds[fileKey][seedKey] = isEmpty(seeds[fileKey][seedKey])
|
||||
? node.value
|
||||
: seeds[key][seedKey] + '\n' + node.value;
|
||||
: seeds[fileKey][seedKey] + '\n' + node.value;
|
||||
}
|
||||
|
||||
function idToData(node, index, parent, seeds) {
|
||||
@ -73,9 +73,9 @@ function idToData(node, index, parent, seeds) {
|
||||
}
|
||||
const codeNode = parent.children[index + 1];
|
||||
if (codeNode && is(codeNode, 'code')) {
|
||||
const key = `index${codeNode.lang}`;
|
||||
if (seeds[key]) throw Error('::id{#id}s must come before code blocks');
|
||||
seeds[key] = defaultFile(codeNode.lang, id);
|
||||
const fileKey = `index${codeNode.lang}`;
|
||||
if (seeds[fileKey]) throw Error('::id{#id}s must come before code blocks');
|
||||
seeds[fileKey] = defaultFile(codeNode.lang, id);
|
||||
} else {
|
||||
throw Error('::id{#id}s must come before code blocks');
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ const ENGLISH_CHALLENGE_NO_FILES = {
|
||||
solutions: ['solution html string'],
|
||||
description: 'description html string',
|
||||
instructions: 'instructions html string',
|
||||
files: []
|
||||
challengeFiles: []
|
||||
};
|
||||
|
||||
exports.ENGLISH_CHALLENGE_NO_FILES = ENGLISH_CHALLENGE_NO_FILES;
|
||||
|
@ -17,19 +17,19 @@ exports.translateComments = (text, lang, dict, codeLang) => {
|
||||
|
||||
exports.translateCommentsInChallenge = (challenge, lang, dict) => {
|
||||
const challClone = cloneDeep(challenge);
|
||||
if (!challClone.files) {
|
||||
if (!challClone.challengeFiles) {
|
||||
console.warn(`Challenge ${challClone.title} has no seed to translate`);
|
||||
} else {
|
||||
Object.keys(challClone.files).forEach(key => {
|
||||
if (challClone.files[key].contents) {
|
||||
challClone.challengeFiles.forEach(challengeFile => {
|
||||
if (challengeFile.contents) {
|
||||
let { text, commentCounts } = this.translateComments(
|
||||
challenge.files[key].contents,
|
||||
challengeFile.contents,
|
||||
lang,
|
||||
dict,
|
||||
challClone.files[key].ext
|
||||
challengeFile.ext
|
||||
);
|
||||
challClone.__commentCounts = commentCounts;
|
||||
challClone.files[key].contents = text;
|
||||
challengeFile.contents = text;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,50 +1,50 @@
|
||||
exports.challengeFiles = {
|
||||
indexcss: {
|
||||
exports.challengeFiles = [
|
||||
{
|
||||
contents: 'some css',
|
||||
error: null,
|
||||
ext: 'css',
|
||||
head: '',
|
||||
history: ['index.css'],
|
||||
key: 'indexcss',
|
||||
fileKey: 'indexcss',
|
||||
name: 'index',
|
||||
path: 'index.css',
|
||||
seed: 'some css',
|
||||
tail: ''
|
||||
},
|
||||
indexhtml: {
|
||||
{
|
||||
contents: 'some html',
|
||||
error: null,
|
||||
ext: 'html',
|
||||
head: '',
|
||||
history: ['index.html'],
|
||||
key: 'indexhtml',
|
||||
fileKey: 'indexhtml',
|
||||
name: 'index',
|
||||
path: 'index.html',
|
||||
seed: 'some html',
|
||||
tail: ''
|
||||
},
|
||||
indexjs: {
|
||||
{
|
||||
contents: 'some js',
|
||||
error: null,
|
||||
ext: 'js',
|
||||
head: '',
|
||||
history: ['index.js'],
|
||||
key: 'indexjs',
|
||||
fileKey: 'indexjs',
|
||||
name: 'index',
|
||||
path: 'index.js',
|
||||
seed: 'some js',
|
||||
tail: ''
|
||||
},
|
||||
indexjsx: {
|
||||
{
|
||||
contents: 'some jsx',
|
||||
error: null,
|
||||
ext: 'jsx',
|
||||
head: '',
|
||||
history: ['index.jsx'],
|
||||
key: 'indexjsx',
|
||||
fileKey: 'indexjsx',
|
||||
name: 'index',
|
||||
path: 'index.jsx',
|
||||
seed: 'some jsx',
|
||||
tail: ''
|
||||
}
|
||||
};
|
||||
];
|
||||
|
@ -37,7 +37,7 @@ function createPoly({ name, ext, contents, history, ...rest } = {}) {
|
||||
name,
|
||||
ext,
|
||||
path: name + '.' + ext,
|
||||
key: name + ext,
|
||||
fileKey: name + ext,
|
||||
contents,
|
||||
error: null
|
||||
};
|
||||
@ -81,7 +81,7 @@ function setExt(ext, poly) {
|
||||
...poly,
|
||||
ext,
|
||||
path: poly.name + '.' + ext,
|
||||
key: poly.name + ext
|
||||
fileKey: poly.name + ext
|
||||
};
|
||||
newPoly.history = [...poly.history, newPoly.path];
|
||||
return newPoly;
|
||||
|
@ -1,5 +1,5 @@
|
||||
exports.toSortedArray = function toSortedArray(challengeFiles) {
|
||||
const xs = Object.values(challengeFiles);
|
||||
const xs = challengeFiles;
|
||||
// TODO: refactor this to use an ext array ['html', 'js', 'css'] and loop over
|
||||
// that.
|
||||
xs.sort((a, b) => {
|
||||
|
@ -9,14 +9,14 @@ describe('sort-files', () => {
|
||||
});
|
||||
it('should not modify the challenges', () => {
|
||||
const sorted = toSortedArray(challengeFiles);
|
||||
const expected = Object.values(challengeFiles);
|
||||
const expected = challengeFiles;
|
||||
expect(sorted).toEqual(expect.arrayContaining(expected));
|
||||
expect(sorted.length).toEqual(expected.length);
|
||||
});
|
||||
|
||||
it('should sort the objects into html, js, css order', () => {
|
||||
const sorted = toSortedArray(challengeFiles);
|
||||
const sortedKeys = sorted.map(({ key }) => key);
|
||||
const sorted = challengeFiles;
|
||||
const sortedKeys = sorted.map(({ fileKey }) => fileKey);
|
||||
const expected = ['indexhtml', 'indexjsx', 'indexjs', 'indexcss'];
|
||||
expect(sortedKeys).toStrictEqual(expected);
|
||||
});
|
||||
|
Reference in New Issue
Block a user