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