feat: display multifile projects (#45220)
* refactor: DRY up certification and ProjectModal * fix: use sensible keys for SolutionViewer * refactor: handle legacy solutions like new ones * refactor: correct CompletedChallenge type * fix: store challengeType for multifile projects * fix: use challengeType to set display type * feat: use dropdown to display project + code * refactor: isOpen -> showCode to avoid a clash We need to be able both show the code and show the completed project * refactor: remove redundant parts of projectPreview * refactor: fix project preview types * feat: wip, using existing modal to show project * feat: show projects on timeline * feat: display projects on time-line * chore: use consistent case for GitHub * fix(a11y): translate show solution/view * refactor: rename showFilesSolution * refactor: use self-closing tag * fix: remove hardcoding (certification + timeline) * fix: remove hardcoding (settings) * test: supply store and mock ga * fix: include challengeType for projects Co-authored-by: Tom <20648924+moT01@users.noreply.github.com> * refactor: remove space Co-authored-by: Tom <20648924+moT01@users.noreply.github.com> * fix: key -> filekey on challenge submission * fix: handle submissions without files Co-authored-by: Tom <20648924+moT01@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
d1d04dbadf
commit
c11bd163b2
@ -239,17 +239,30 @@ export function modernChallengeCompleted(req, res, next) {
|
||||
const completedDate = Date.now();
|
||||
const { id, files, challengeType } = req.body;
|
||||
|
||||
const data = {
|
||||
const completedChallenge = {
|
||||
id,
|
||||
files,
|
||||
completedDate
|
||||
};
|
||||
|
||||
if (challengeType === 14) {
|
||||
data.isManuallyApproved = false;
|
||||
completedChallenge.isManuallyApproved = false;
|
||||
}
|
||||
|
||||
const { alreadyCompleted, updateData } = buildUserUpdate(user, id, data);
|
||||
// We only need to know the challenge type if it's a project. If it's a
|
||||
// step or normal challenge we can avoid storing in the database.
|
||||
if (
|
||||
jsCertProjectIds.includes(id) ||
|
||||
multiFileCertProjectIds.includes(id)
|
||||
) {
|
||||
completedChallenge.challengeType = challengeType;
|
||||
}
|
||||
|
||||
const { alreadyCompleted, updateData } = buildUserUpdate(
|
||||
user,
|
||||
id,
|
||||
completedChallenge
|
||||
);
|
||||
const points = alreadyCompleted ? user.points : user.points + 1;
|
||||
const updatePromise = new Promise((resolve, reject) =>
|
||||
user.updateAttributes(updateData, err => {
|
||||
|
@ -8,6 +8,7 @@
|
||||
"edit": "Edit",
|
||||
"show-code": "Show Code",
|
||||
"show-solution": "Show Solution",
|
||||
"show-project": "Show Project",
|
||||
"frontend": "Front End",
|
||||
"backend": "Back End",
|
||||
"view": "View",
|
||||
|
@ -1,35 +1,44 @@
|
||||
import { find, first } from 'lodash-es';
|
||||
import React, { useState } from 'react';
|
||||
import '../components/layouts/project-links.css';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ProjectModal from '../components/SolutionViewer/ProjectModal';
|
||||
import { Spacer, Link } from '../components/helpers';
|
||||
import { ChallengeFiles, CompletedChallenge, User } from '../redux/prop-types';
|
||||
import { CompletedChallenge, User } from '../redux/prop-types';
|
||||
import {
|
||||
projectMap,
|
||||
legacyProjectMap
|
||||
} from '../resources/cert-and-project-map';
|
||||
|
||||
import { SolutionDisplayWidget } from '../components/solution-display-widget';
|
||||
import ProjectPreviewModal from '../templates/Challenges/components/project-preview-modal';
|
||||
|
||||
import { openModal } from '../templates/Challenges/redux';
|
||||
|
||||
import '../components/layouts/project-links.css';
|
||||
import { regeneratePathAndHistory } from '../../../utils/polyvinyl';
|
||||
interface ShowProjectLinksProps {
|
||||
certName: string;
|
||||
name: string;
|
||||
user: User;
|
||||
openModal: (arg: string) => void;
|
||||
}
|
||||
|
||||
type SolutionState = {
|
||||
projectTitle: string;
|
||||
challengeFiles: ChallengeFiles;
|
||||
solution: CompletedChallenge['solution'];
|
||||
isOpen: boolean;
|
||||
completedChallenge: CompletedChallenge | null;
|
||||
showCode: boolean;
|
||||
};
|
||||
|
||||
const initSolutionState: SolutionState = {
|
||||
projectTitle: '',
|
||||
challengeFiles: null,
|
||||
solution: '',
|
||||
isOpen: false
|
||||
completedChallenge: null,
|
||||
showCode: false
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openModal
|
||||
};
|
||||
|
||||
const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
|
||||
@ -41,32 +50,41 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
|
||||
|
||||
const getProjectSolution = (projectId: string, projectTitle: string) => {
|
||||
const {
|
||||
user: { completedChallenges }
|
||||
user: { completedChallenges },
|
||||
openModal
|
||||
} = props;
|
||||
const completedProject = find(
|
||||
completedChallenges,
|
||||
({ id }) => projectId === id
|
||||
) as CompletedChallenge;
|
||||
);
|
||||
|
||||
if (!completedProject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { solution, challengeFiles } = completedProject;
|
||||
const showFilesSolution = () =>
|
||||
const showUserCode = () =>
|
||||
setSolutionState({
|
||||
projectTitle,
|
||||
challengeFiles,
|
||||
solution,
|
||||
isOpen: true
|
||||
completedChallenge: completedProject,
|
||||
showCode: true
|
||||
});
|
||||
|
||||
const showProjectPreview = () => {
|
||||
setSolutionState({
|
||||
projectTitle,
|
||||
completedChallenge: completedProject,
|
||||
showCode: false
|
||||
});
|
||||
openModal('projectPreview');
|
||||
};
|
||||
|
||||
return (
|
||||
<SolutionDisplayWidget
|
||||
completedChallenge={completedProject}
|
||||
dataCy={`${projectTitle} solution`}
|
||||
displayContext='certification'
|
||||
showFilesSolution={showFilesSolution}
|
||||
showUserCode={showUserCode}
|
||||
showProjectPreview={showProjectPreview}
|
||||
></SolutionDisplayWidget>
|
||||
);
|
||||
};
|
||||
@ -121,7 +139,18 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
|
||||
name,
|
||||
user: { username }
|
||||
} = props;
|
||||
const { challengeFiles, isOpen, projectTitle, solution } = solutionState;
|
||||
const { completedChallenge, showCode, projectTitle } = solutionState;
|
||||
|
||||
const challengeData: CompletedChallenge | null = completedChallenge
|
||||
? {
|
||||
...completedChallenge,
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
challengeFiles:
|
||||
completedChallenge?.challengeFiles?.map(regeneratePathAndHistory) ??
|
||||
null
|
||||
}
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{t(
|
||||
@ -133,17 +162,21 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
|
||||
<Spacer />
|
||||
<ul>{renderProjectsFor(certName)}</ul>
|
||||
<Spacer />
|
||||
{isOpen ? (
|
||||
<ProjectModal
|
||||
challengeFiles={challengeFiles}
|
||||
handleSolutionModalHide={handleSolutionModalHide}
|
||||
isOpen={isOpen}
|
||||
projectTitle={projectTitle}
|
||||
// 'solution' is theoretically never 'null', if it a JsAlgoData cert
|
||||
// which is the only time we use the modal
|
||||
solution={solution as undefined | string}
|
||||
/>
|
||||
) : null}
|
||||
<ProjectModal
|
||||
challengeFiles={completedChallenge?.challengeFiles ?? null}
|
||||
handleSolutionModalHide={handleSolutionModalHide}
|
||||
isOpen={showCode}
|
||||
projectTitle={projectTitle}
|
||||
// 'solution' is theoretically never 'null', if it a JsAlgoData cert
|
||||
// which is the only time we use the modal
|
||||
solution={completedChallenge?.solution as undefined | string}
|
||||
/>
|
||||
<ProjectPreviewModal
|
||||
challengeData={challengeData}
|
||||
closeText={t('buttons.close')}
|
||||
previewTitle={projectTitle}
|
||||
showProjectPreview={true}
|
||||
/>
|
||||
<Trans i18nKey='certification.project.footnote'>
|
||||
If you suspect that any of these projects violate the{' '}
|
||||
<a
|
||||
@ -169,4 +202,4 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
|
||||
|
||||
ShowProjectLinks.displayName = 'ShowProjectLinks';
|
||||
|
||||
export default ShowProjectLinks;
|
||||
export default connect(null, mapDispatchToProps)(ShowProjectLinks);
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChallengeFiles } from '../../redux/prop-types';
|
||||
import { CompletedChallenge } from '../../redux/prop-types';
|
||||
import SolutionViewer from './SolutionViewer';
|
||||
|
||||
type ProjectModalProps = {
|
||||
challengeFiles: ChallengeFiles;
|
||||
challengeFiles: CompletedChallenge['challengeFiles'] | null;
|
||||
handleSolutionModalHide: () => void;
|
||||
isOpen: boolean;
|
||||
projectTitle: string;
|
||||
@ -27,11 +27,9 @@ const ProjectModal = ({
|
||||
onHide={handleSolutionModalHide}
|
||||
show={isOpen}
|
||||
>
|
||||
<Modal.Header className='this-one?' closeButton={true}>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='solution-viewer-modal-title'>
|
||||
{t('settings.labels.solution-for', {
|
||||
projectTitle: projectTitle
|
||||
})}
|
||||
{t('settings.labels.solution-for', { projectTitle })}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
|
@ -1,66 +1,47 @@
|
||||
import { Panel } from '@freecodecamp/react-bootstrap';
|
||||
import Prism from 'prismjs';
|
||||
import React from 'react';
|
||||
import { ChallengeFile, ChallengeFiles } from '../../redux/prop-types';
|
||||
import { ChallengeFile } from '../../redux/prop-types';
|
||||
|
||||
type SolutionViewerProps = {
|
||||
challengeFiles: ChallengeFiles;
|
||||
type Props = {
|
||||
challengeFiles: Solution[] | null;
|
||||
solution?: string;
|
||||
};
|
||||
type Solution = Pick<ChallengeFile, 'ext' | 'contents' | 'fileKey'>;
|
||||
|
||||
function SolutionViewer({ challengeFiles, solution }: Props) {
|
||||
const isLegacy = !challengeFiles || !challengeFiles.length;
|
||||
const solutions = isLegacy
|
||||
? [
|
||||
{
|
||||
ext: 'js',
|
||||
contents:
|
||||
solution ?? '// The solution is not available for this project',
|
||||
fileKey: 'script.js'
|
||||
}
|
||||
]
|
||||
: challengeFiles;
|
||||
|
||||
function SolutionViewer({
|
||||
challengeFiles,
|
||||
solution = '// The solution is not available for this project'
|
||||
}: SolutionViewerProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{challengeFiles?.length ? (
|
||||
challengeFiles.map((challengeFile: ChallengeFile) => (
|
||||
<Panel
|
||||
bsStyle='primary'
|
||||
className='solution-viewer'
|
||||
key={challengeFile}
|
||||
>
|
||||
<Panel.Heading>{challengeFile.ext.toUpperCase()}</Panel.Heading>
|
||||
|
||||
<Panel.Body>
|
||||
<pre>
|
||||
<code
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Prism.highlight(
|
||||
challengeFile.contents.trim(),
|
||||
Prism.languages[challengeFile.ext],
|
||||
''
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
</Panel.Body>
|
||||
</Panel>
|
||||
))
|
||||
) : (
|
||||
<Panel
|
||||
bsStyle='primary'
|
||||
className='solution-viewer'
|
||||
key={solution.slice(0, 10)}
|
||||
>
|
||||
<Panel.Heading>JS</Panel.Heading>
|
||||
{solutions.map(({ fileKey, ext, contents }) => (
|
||||
<Panel bsStyle='primary' className='solution-viewer' key={fileKey}>
|
||||
<Panel.Heading>{ext.toUpperCase()}</Panel.Heading>
|
||||
<Panel.Body>
|
||||
<pre>
|
||||
<code
|
||||
className='language-markup'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Prism.highlight(
|
||||
solution.trim(),
|
||||
Prism.languages.js,
|
||||
'javascript'
|
||||
contents.trim(),
|
||||
Prism.languages[ext],
|
||||
ext
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
</Panel.Body>
|
||||
</Panel>
|
||||
)}
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useStaticQuery } from 'gatsby';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../utils/test-utils';
|
||||
import { createStore } from '../../../redux/createStore';
|
||||
import TimeLine from './time-line';
|
||||
|
||||
jest.mock('react-ga');
|
||||
const store = createStore();
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
useStaticQuery.mockImplementationOnce(() => ({
|
||||
allChallengeNode: {
|
||||
edges: [
|
||||
@ -50,8 +55,8 @@ beforeEach(() => {
|
||||
|
||||
describe('<TimeLine />', () => {
|
||||
it('Render button when only solution is present', () => {
|
||||
// @ts-ignore
|
||||
render(<TimeLine {...propsForOnlySolution} />);
|
||||
// @ts-expect-error
|
||||
render(<TimeLine {...propsForOnlySolution} />, store);
|
||||
const showViewButton = screen.getByRole('link', { name: 'buttons.view' });
|
||||
expect(showViewButton).toHaveAttribute(
|
||||
'href',
|
||||
@ -60,8 +65,8 @@ describe('<TimeLine />', () => {
|
||||
});
|
||||
|
||||
it('Render button when both githubLink and solution is present', () => {
|
||||
// @ts-ignore
|
||||
render(<TimeLine {...propsForOnlySolution} />);
|
||||
// @ts-expect-error
|
||||
render(<TimeLine {...propsForOnlySolution} />, store);
|
||||
|
||||
const menuItems = screen.getAllByRole('menuitem');
|
||||
expect(menuItems).toHaveLength(2);
|
||||
@ -76,8 +81,8 @@ describe('<TimeLine />', () => {
|
||||
});
|
||||
|
||||
it('rendering the correct button when files is present', () => {
|
||||
// @ts-ignore
|
||||
render(<TimeLine {...propsForOnlySolution} />);
|
||||
// @ts-expect-error
|
||||
render(<TimeLine {...propsForOnlySolution} />, store);
|
||||
|
||||
const button = screen.getByText('buttons.show-code');
|
||||
expect(button).toBeInTheDocument();
|
||||
|
@ -1,10 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { Button, Modal, Table } from '@freecodecamp/react-bootstrap';
|
||||
import Loadable from '@loadable/component';
|
||||
import { useStaticQuery, graphql } from 'gatsby';
|
||||
import { reverse, sortBy } from 'lodash-es';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { TFunction, withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import envData from '../../../../../config/env.json';
|
||||
import { langCodes } from '../../../../../config/i18n/all-langs';
|
||||
@ -13,8 +13,11 @@ import {
|
||||
getPathFromID,
|
||||
getTitleFromId
|
||||
} from '../../../../../utils';
|
||||
import { regeneratePathAndHistory } from '../../../../../utils/polyvinyl';
|
||||
import CertificationIcon from '../../../assets/icons/certification-icon';
|
||||
import { ChallengeFiles, CompletedChallenge } from '../../../redux/prop-types';
|
||||
import { CompletedChallenge } from '../../../redux/prop-types';
|
||||
import ProjectPreviewModal from '../../../templates/Challenges/components/project-preview-modal';
|
||||
import { openModal } from '../../../templates/Challenges/redux';
|
||||
import { FullWidthRow, Link } from '../../helpers';
|
||||
import { SolutionDisplayWidget } from '../../solution-display-widget';
|
||||
import TimelinePagination from './timeline-pagination';
|
||||
@ -25,6 +28,10 @@ const SolutionViewer = Loadable(
|
||||
() => import('../../SolutionViewer/SolutionViewer')
|
||||
);
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openModal
|
||||
};
|
||||
|
||||
const { clientLocale } = envData as { clientLocale: keyof typeof langCodes };
|
||||
const localeCode = langCodes[clientLocale];
|
||||
|
||||
@ -33,46 +40,53 @@ const ITEMS_PER_PAGE = 15;
|
||||
|
||||
interface TimelineProps {
|
||||
completedMap: CompletedChallenge[];
|
||||
openModal: (arg: string) => void;
|
||||
t: TFunction;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface TimelineInnerProps extends TimelineProps {
|
||||
idToNameMap: Map<string, string>;
|
||||
idToNameMap: Map<string, NameMap>;
|
||||
sortedTimeline: CompletedChallenge[];
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
interface NameMap {
|
||||
challengeTitle: string;
|
||||
challengePath: string;
|
||||
}
|
||||
|
||||
function TimelineInner({
|
||||
completedMap,
|
||||
idToNameMap,
|
||||
openModal,
|
||||
sortedTimeline,
|
||||
totalPages,
|
||||
completedMap,
|
||||
t,
|
||||
username
|
||||
}: TimelineInnerProps) {
|
||||
const [solutionToView, setSolutionToView] = useState<string | null>(null);
|
||||
const [projectTitle, setProjectTitle] = useState('');
|
||||
const [solutionOpen, setSolutionOpen] = useState(false);
|
||||
const [pageNo, setPageNo] = useState(1);
|
||||
const [solution, setSolution] = useState<string | null>(null);
|
||||
const [challengeFiles, setChallengeFiles] = useState<ChallengeFiles>(null);
|
||||
const [completedChallenge, setCompletedChallenge] =
|
||||
useState<CompletedChallenge | null>(null);
|
||||
|
||||
function viewSolution(
|
||||
id: string,
|
||||
solution_: string | undefined | null,
|
||||
challengeFiles_: ChallengeFiles
|
||||
): void {
|
||||
setSolutionToView(id);
|
||||
function viewSolution(completedChallenge: CompletedChallenge): void {
|
||||
setCompletedChallenge(completedChallenge);
|
||||
setSolutionOpen(true);
|
||||
setSolution(solution_ ?? '');
|
||||
setChallengeFiles(challengeFiles_);
|
||||
}
|
||||
|
||||
function viewProject(completedChallenge: CompletedChallenge): void {
|
||||
setCompletedChallenge(completedChallenge);
|
||||
setProjectTitle(
|
||||
idToNameMap.get(completedChallenge.id)?.challengeTitle ?? ''
|
||||
);
|
||||
openModal('projectPreview');
|
||||
}
|
||||
|
||||
function closeSolution(): void {
|
||||
setSolutionToView(null);
|
||||
setSolutionOpen(false);
|
||||
setSolution(null);
|
||||
setChallengeFiles(null);
|
||||
setCompletedChallenge(null);
|
||||
}
|
||||
|
||||
function firstPage(): void {
|
||||
@ -91,11 +105,11 @@ function TimelineInner({
|
||||
function renderViewButton(
|
||||
completedChallenge: CompletedChallenge
|
||||
): React.ReactNode {
|
||||
const { id, solution, challengeFiles } = completedChallenge;
|
||||
return (
|
||||
<SolutionDisplayWidget
|
||||
completedChallenge={completedChallenge}
|
||||
showFilesSolution={() => viewSolution(id, solution, challengeFiles)}
|
||||
showUserCode={() => viewSolution(completedChallenge)}
|
||||
showProjectPreview={() => viewProject(completedChallenge)}
|
||||
displayContext={'timeline'}
|
||||
></SolutionDisplayWidget>
|
||||
);
|
||||
@ -135,73 +149,90 @@ function TimelineInner({
|
||||
);
|
||||
}
|
||||
|
||||
const id = solutionToView;
|
||||
const challengeData: CompletedChallenge | null = completedChallenge
|
||||
? {
|
||||
...completedChallenge,
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
challengeFiles:
|
||||
completedChallenge?.challengeFiles?.map(regeneratePathAndHistory) ??
|
||||
null
|
||||
}
|
||||
: null;
|
||||
|
||||
const id = challengeData?.id;
|
||||
const startIndex = (pageNo - 1) * ITEMS_PER_PAGE;
|
||||
const endIndex = pageNo * ITEMS_PER_PAGE;
|
||||
|
||||
return (
|
||||
<FullWidthRow>
|
||||
<h2 className='text-center'>{t('profile.timeline')}</h2>
|
||||
{completedMap.length === 0 ? (
|
||||
<p className='text-center'>
|
||||
{t('profile.none-completed')}
|
||||
<Link to='/learn'>{t('profile.get-started')}</Link>
|
||||
</p>
|
||||
) : (
|
||||
<Table condensed={true} striped={true}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('profile.challenge')}</th>
|
||||
<th>{t('settings.labels.solution')}</th>
|
||||
<th className='text-center'>{t('profile.completed')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTimeline.slice(startIndex, endIndex).map(renderCompletion)}
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
{id && (
|
||||
<Modal
|
||||
aria-labelledby='contained-modal-title'
|
||||
onHide={closeSolution}
|
||||
show={solutionOpen}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='contained-modal-title'>
|
||||
{`${username}'s Solution to ${
|
||||
// @ts-expect-error Need better TypeDef for this
|
||||
idToNameMap.get(id).challengeTitle as string
|
||||
}`}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<SolutionViewer
|
||||
challengeFiles={challengeFiles}
|
||||
solution={solution ?? ''}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={closeSolution}>{t('buttons.close')}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)}
|
||||
{totalPages > 1 && (
|
||||
<TimelinePagination
|
||||
firstPage={firstPage}
|
||||
lastPage={lastPage}
|
||||
nextPage={nextPage}
|
||||
pageNo={pageNo}
|
||||
prevPage={prevPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
)}
|
||||
</FullWidthRow>
|
||||
<>
|
||||
<FullWidthRow>
|
||||
<h2 className='text-center'>{t('profile.timeline')}</h2>
|
||||
{completedMap.length === 0 ? (
|
||||
<p className='text-center'>
|
||||
{t('profile.none-completed')}
|
||||
<Link to='/learn'>{t('profile.get-started')}</Link>
|
||||
</p>
|
||||
) : (
|
||||
<Table condensed={true} striped={true}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('profile.challenge')}</th>
|
||||
<th>{t('settings.labels.solution')}</th>
|
||||
<th className='text-center'>{t('profile.completed')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTimeline.slice(startIndex, endIndex).map(renderCompletion)}
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
{id && (
|
||||
<Modal
|
||||
aria-labelledby='contained-modal-title'
|
||||
onHide={closeSolution}
|
||||
show={solutionOpen}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='contained-modal-title'>
|
||||
{`${username}'s Solution to ${
|
||||
idToNameMap.get(id)?.challengeTitle ?? ''
|
||||
}`}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<SolutionViewer
|
||||
challengeFiles={challengeData.challengeFiles}
|
||||
solution={challengeData.solution ?? ''}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={closeSolution}>{t('buttons.close')}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)}
|
||||
{totalPages > 1 && (
|
||||
<TimelinePagination
|
||||
firstPage={firstPage}
|
||||
lastPage={lastPage}
|
||||
nextPage={nextPage}
|
||||
pageNo={pageNo}
|
||||
prevPage={prevPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
)}
|
||||
</FullWidthRow>
|
||||
<ProjectPreviewModal
|
||||
challengeData={challengeData}
|
||||
closeText={t('buttons.close')}
|
||||
previewTitle={projectTitle}
|
||||
showProjectPreview={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* 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, NameMap> {
|
||||
const {
|
||||
allChallengeNode: { edges }
|
||||
} = useStaticQuery(graphql`
|
||||
@ -273,4 +304,4 @@ const Timeline = (props: TimelineProps): JSX.Element => {
|
||||
|
||||
Timeline.displayName = 'Timeline';
|
||||
|
||||
export default withTranslation()(Timeline);
|
||||
export default connect(null, mapDispatchToProps)(withTranslation()(Timeline));
|
||||
|
@ -5,8 +5,12 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import ScrollableAnchor, { configureAnchors } from 'react-scrollable-anchor';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { regeneratePathAndHistory } from '../../../../utils/polyvinyl';
|
||||
import ProjectPreviewModal from '../../templates/Challenges/components/project-preview-modal';
|
||||
import { openModal } from '../../templates/Challenges/redux';
|
||||
import {
|
||||
projectMap,
|
||||
legacyProjectMap
|
||||
@ -50,11 +54,16 @@ const propTypes = {
|
||||
isRelationalDatabaseCertV8: PropTypes.bool,
|
||||
isRespWebDesignCert: PropTypes.bool,
|
||||
isSciCompPyCertV7: PropTypes.bool,
|
||||
openModal: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
verifyCert: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openModal
|
||||
};
|
||||
|
||||
const certifications = Object.keys(projectMap);
|
||||
const legacyCertifications = Object.keys(legacyProjectMap);
|
||||
const isCertSelector = ({
|
||||
@ -161,7 +170,7 @@ export class CertificationSettings extends Component {
|
||||
getUserIsCertMap = () => isCertMapSelector(this.props);
|
||||
|
||||
getProjectSolution = (projectId, projectTitle) => {
|
||||
const { completedChallenges } = this.props;
|
||||
const { completedChallenges, openModal } = this.props;
|
||||
const completedProject = find(
|
||||
completedChallenges,
|
||||
({ id }) => projectId === id
|
||||
@ -171,8 +180,7 @@ export class CertificationSettings extends Component {
|
||||
}
|
||||
|
||||
const { solution, challengeFiles } = completedProject;
|
||||
|
||||
const onClickHandler = () =>
|
||||
const showUserCode = () =>
|
||||
this.setState({
|
||||
solutionViewer: {
|
||||
projectTitle,
|
||||
@ -182,11 +190,31 @@ export class CertificationSettings extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
const challengeData = completedProject
|
||||
? {
|
||||
...completedProject,
|
||||
challengeFiles:
|
||||
completedProject?.challengeFiles?.map(regeneratePathAndHistory) ??
|
||||
null
|
||||
}
|
||||
: null;
|
||||
|
||||
const showProjectPreview = () => {
|
||||
this.setState({
|
||||
projectViewer: {
|
||||
previewTitle: projectTitle,
|
||||
challengeData
|
||||
}
|
||||
});
|
||||
openModal('projectPreview');
|
||||
};
|
||||
|
||||
return (
|
||||
<SolutionDisplayWidget
|
||||
completedChallenge={completedProject}
|
||||
dataCy={projectTitle}
|
||||
showFilesSolution={onClickHandler}
|
||||
showUserCode={showUserCode}
|
||||
showProjectPreview={showProjectPreview}
|
||||
displayContext={'settings'}
|
||||
></SolutionDisplayWidget>
|
||||
);
|
||||
@ -361,11 +389,9 @@ export class CertificationSettings extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
solutionViewer: { challengeFiles, solution, isOpen, projectTitle }
|
||||
} = this.state;
|
||||
|
||||
const { solutionViewer, projectViewer } = this.state;
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<ScrollableAnchor id='certification-settings'>
|
||||
<section className='certification-settings'>
|
||||
@ -378,16 +404,15 @@ export class CertificationSettings extends Component {
|
||||
{legacyCertifications.map(certName =>
|
||||
this.renderCertifications(certName, legacyProjectMap)
|
||||
)}
|
||||
{isOpen ? (
|
||||
<ProjectModal
|
||||
challengeFiles={challengeFiles}
|
||||
handleSolutionModalHide={this.handleSolutionModalHide}
|
||||
isOpen={isOpen}
|
||||
projectTitle={projectTitle}
|
||||
solution={solution}
|
||||
t={t}
|
||||
/>
|
||||
) : null}
|
||||
<ProjectModal
|
||||
{...solutionViewer}
|
||||
handleSolutionModalHide={this.handleSolutionModalHide}
|
||||
/>
|
||||
<ProjectPreviewModal
|
||||
{...projectViewer}
|
||||
closeText={t('buttons.close')}
|
||||
showProjectPreview={true}
|
||||
/>
|
||||
</section>
|
||||
</ScrollableAnchor>
|
||||
);
|
||||
@ -397,4 +422,7 @@ export class CertificationSettings extends Component {
|
||||
CertificationSettings.displayName = 'CertificationSettings';
|
||||
CertificationSettings.propTypes = propTypes;
|
||||
|
||||
export default withTranslation()(CertificationSettings);
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(withTranslation()(CertificationSettings));
|
||||
|
@ -11,22 +11,22 @@ import { getSolutionDisplayType } from '../../utils/solution-display-type';
|
||||
interface Props {
|
||||
completedChallenge: CompletedChallenge;
|
||||
dataCy?: string;
|
||||
showFilesSolution: () => void;
|
||||
showUserCode: () => void;
|
||||
showProjectPreview?: () => void;
|
||||
displayContext: 'timeline' | 'settings' | 'certification';
|
||||
}
|
||||
|
||||
export function SolutionDisplayWidget({
|
||||
completedChallenge,
|
||||
dataCy,
|
||||
showFilesSolution,
|
||||
showUserCode,
|
||||
showProjectPreview,
|
||||
displayContext
|
||||
}: Props) {
|
||||
const { id, solution, githubLink } = completedChallenge;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dropdownTitle =
|
||||
displayContext === 'settings' ? 'Show Solutions' : 'View';
|
||||
const projectLinkText =
|
||||
const showOrViewText =
|
||||
displayContext === 'settings'
|
||||
? t('buttons.show-solution')
|
||||
: t('buttons.view');
|
||||
@ -35,7 +35,7 @@ export function SolutionDisplayWidget({
|
||||
<button
|
||||
className='project-link-button-override'
|
||||
data-cy={dataCy}
|
||||
onClick={showFilesSolution}
|
||||
onClick={showUserCode}
|
||||
>
|
||||
{t('certification.project.solution')}
|
||||
</button>
|
||||
@ -64,18 +64,35 @@ export function SolutionDisplayWidget({
|
||||
const MissingSolutionComponentForCertification = (
|
||||
<>{t('certification.project.no-solution')}</>
|
||||
);
|
||||
const ShowFilesSolution = (
|
||||
const ShowUserCode = (
|
||||
<Button
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
className='btn-invert'
|
||||
data-cy={dataCy}
|
||||
id={`btn-for-${id}`}
|
||||
onClick={showFilesSolution}
|
||||
onClick={showUserCode}
|
||||
>
|
||||
{t('buttons.show-code')}
|
||||
</Button>
|
||||
);
|
||||
const ShowMultifileProjectSolution = (
|
||||
<DropdownButton
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
className='btn-invert'
|
||||
id={`dropdown-for-${id}`}
|
||||
title={t('buttons.view')}
|
||||
>
|
||||
<MenuItem bsStyle='primary' onClick={showUserCode}>
|
||||
{t('buttons.show-code')}
|
||||
</MenuItem>
|
||||
<MenuItem bsStyle='primary' onClick={showProjectPreview}>
|
||||
{t('buttons.show-project')}
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
);
|
||||
|
||||
const ShowProjectAndGithubLinks = (
|
||||
<div className='solutions-dropdown'>
|
||||
<DropdownButton
|
||||
@ -83,7 +100,7 @@ export function SolutionDisplayWidget({
|
||||
bsStyle='primary'
|
||||
className='btn-invert'
|
||||
id={`dropdown-for-${id}`}
|
||||
title={dropdownTitle}
|
||||
title={showOrViewText}
|
||||
>
|
||||
<MenuItem
|
||||
bsStyle='primary'
|
||||
@ -114,7 +131,7 @@ export function SolutionDisplayWidget({
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
{projectLinkText}
|
||||
{showOrViewText}
|
||||
</Button>
|
||||
);
|
||||
const MissingSolutionComponent =
|
||||
@ -125,14 +142,16 @@ export function SolutionDisplayWidget({
|
||||
const displayComponents =
|
||||
displayContext === 'certification'
|
||||
? {
|
||||
showFilesSolution: ShowFilesSolutionForCertification,
|
||||
showProjectAndGitHubLinks: ShowProjectAndGithubLinkForCertification,
|
||||
showUserCode: ShowFilesSolutionForCertification,
|
||||
showMultifileProjectSolution: ShowMultifileProjectSolution,
|
||||
showProjectAndGithubLinks: ShowProjectAndGithubLinkForCertification,
|
||||
showProjectLink: ShowProjectLinkForCertification,
|
||||
none: MissingSolutionComponentForCertification
|
||||
}
|
||||
: {
|
||||
showFilesSolution: ShowFilesSolution,
|
||||
showProjectAndGitHubLinks: ShowProjectAndGithubLinks,
|
||||
showUserCode: ShowUserCode,
|
||||
showMultifileProjectSolution: ShowMultifileProjectSolution,
|
||||
showProjectAndGithubLinks: ShowProjectAndGithubLinks,
|
||||
showProjectLink: ShowProjectLink,
|
||||
none: MissingSolutionComponent
|
||||
};
|
||||
|
@ -316,7 +316,9 @@ export type CompletedChallenge = {
|
||||
githubLink?: string;
|
||||
challengeType?: number;
|
||||
completedDate: number;
|
||||
challengeFiles: ChallengeFiles;
|
||||
challengeFiles:
|
||||
| Pick<ChallengeFile, 'contents' | 'ext' | 'fileKey' | 'name'>[]
|
||||
| null;
|
||||
};
|
||||
|
||||
export type Ext = 'js' | 'html' | 'css' | 'jsx';
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
ChallengeFiles,
|
||||
ChallengeMeta,
|
||||
ChallengeNode,
|
||||
CompletedChallenge,
|
||||
ResizeProps,
|
||||
Test
|
||||
} from '../../../redux/prop-types';
|
||||
@ -29,9 +30,7 @@ import HelpModal from '../components/help-modal';
|
||||
import Notes from '../components/notes';
|
||||
import Output from '../components/output';
|
||||
import Preview from '../components/preview';
|
||||
import ProjectPreviewModal, {
|
||||
PreviewConfig
|
||||
} from '../components/project-preview-modal';
|
||||
import ProjectPreviewModal from '../components/project-preview-modal';
|
||||
import SidePanel from '../components/side-panel';
|
||||
import VideoModal from '../components/video-modal';
|
||||
import {
|
||||
@ -97,7 +96,10 @@ interface ShowClassicProps {
|
||||
output: string[];
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMeta;
|
||||
projectPreview: PreviewConfig & { showProjectPreview: boolean };
|
||||
projectPreview: {
|
||||
challengeData: CompletedChallenge;
|
||||
showProjectPreview: boolean;
|
||||
};
|
||||
};
|
||||
t: TFunction;
|
||||
tests: Test[];
|
||||
@ -367,7 +369,6 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
}
|
||||
} = this.props;
|
||||
const { description, title } = this.getChallenge();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return (
|
||||
challengeFiles && (
|
||||
<MultifileEditor
|
||||
@ -430,7 +431,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
executeChallenge,
|
||||
pageContext: {
|
||||
challengeMeta: { nextChallengePath, prevChallengePath },
|
||||
projectPreview
|
||||
projectPreview: { challengeData, showProjectPreview }
|
||||
},
|
||||
challengeFiles,
|
||||
t
|
||||
@ -494,7 +495,12 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
<HelpModal />
|
||||
<VideoModal videoUrl={this.getVideoUrl()} />
|
||||
<ResetModal />
|
||||
<ProjectPreviewModal previewConfig={projectPreview} />
|
||||
<ProjectPreviewModal
|
||||
challengeData={challengeData}
|
||||
closeText={t('buttons.start-coding')}
|
||||
previewTitle={t('learn.project-preview-title')}
|
||||
showProjectPreview={showProjectPreview}
|
||||
/>
|
||||
</LearnLayout>
|
||||
</Hotkeys>
|
||||
);
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import type { ChallengeFile, Required } from '../../../redux/prop-types';
|
||||
import type { CompletedChallenge } from '../../../redux/prop-types';
|
||||
import {
|
||||
closeModal,
|
||||
setEditorFocusability,
|
||||
@ -15,19 +14,20 @@ import Preview from './preview';
|
||||
|
||||
import './project-preview-modal.css';
|
||||
|
||||
export interface PreviewConfig {
|
||||
challengeType: boolean;
|
||||
challengeFiles: ChallengeFile[];
|
||||
required: Required;
|
||||
template: string;
|
||||
interface ProjectPreviewMountedPayload {
|
||||
challengeData: CompletedChallenge | null;
|
||||
showProjectPreview: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
closeModal: (arg: string) => void;
|
||||
isOpen: boolean;
|
||||
projectPreviewMounted: (previewConfig: PreviewConfig) => void;
|
||||
previewConfig: PreviewConfig;
|
||||
projectPreviewMounted: (payload: ProjectPreviewMountedPayload) => void;
|
||||
challengeData: CompletedChallenge | null;
|
||||
setEditorFocusability: (focusability: boolean) => void;
|
||||
showProjectPreview: boolean;
|
||||
previewTitle: string;
|
||||
closeText: string;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: unknown) => ({
|
||||
@ -39,14 +39,16 @@ const mapDispatchToProps = {
|
||||
projectPreviewMounted
|
||||
};
|
||||
|
||||
export function ProjectPreviewModal({
|
||||
function ProjectPreviewModal({
|
||||
closeModal,
|
||||
isOpen,
|
||||
projectPreviewMounted,
|
||||
previewConfig,
|
||||
setEditorFocusability
|
||||
challengeData,
|
||||
setEditorFocusability,
|
||||
showProjectPreview,
|
||||
previewTitle,
|
||||
closeText
|
||||
}: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
if (isOpen) setEditorFocusability(false);
|
||||
});
|
||||
@ -66,17 +68,15 @@ export function ProjectPreviewModal({
|
||||
className='project-preview-modal-header fcc-modal'
|
||||
closeButton={true}
|
||||
>
|
||||
<Modal.Title className='text-center'>
|
||||
{t('learn.project-preview-title')}
|
||||
</Modal.Title>
|
||||
<Modal.Title className='text-center'>{previewTitle}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='project-preview-modal-body text-center'>
|
||||
{/* remove type assertion once frame.js has been migrated to TS */}
|
||||
<Preview
|
||||
previewId={projectPreviewId as string}
|
||||
previewMounted={() => {
|
||||
projectPreviewMounted(previewConfig);
|
||||
}}
|
||||
previewMounted={() =>
|
||||
projectPreviewMounted({ challengeData, showProjectPreview })
|
||||
}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
@ -89,7 +89,7 @@ export function ProjectPreviewModal({
|
||||
setEditorFocusability(true);
|
||||
}}
|
||||
>
|
||||
{t('buttons.start-coding')}
|
||||
{closeText}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
@ -35,10 +35,18 @@ function postChallenge(update, username) {
|
||||
const saveChallenge = postUpdate$(update).pipe(
|
||||
retry(3),
|
||||
switchMap(({ points }) => {
|
||||
// TODO: do this all in ajax.ts
|
||||
const payloadWithClientProperties = {
|
||||
...omit(update.payload, ['files']),
|
||||
challengeFiles: update.payload.files ?? null
|
||||
...omit(update.payload, ['files'])
|
||||
};
|
||||
if (update.payload.files) {
|
||||
payloadWithClientProperties.challengeFiles = update.payload.files.map(
|
||||
({ key, ...rest }) => ({
|
||||
...rest,
|
||||
fileKey: key
|
||||
})
|
||||
);
|
||||
}
|
||||
return of(
|
||||
submitComplete({
|
||||
username,
|
||||
|
@ -27,6 +27,11 @@ export const withChallenges = {
|
||||
challengeFiles
|
||||
}
|
||||
|
||||
export const multifileSolution = {
|
||||
...withChallenges,
|
||||
challengeType: 14
|
||||
}
|
||||
|
||||
export const onlyGithubLink = {
|
||||
...baseChallenge,
|
||||
githubLink: 'https://some.thing'
|
||||
|
@ -1,7 +1,8 @@
|
||||
import {
|
||||
bothLinks,
|
||||
legacySolution,
|
||||
invalidGithubLink,
|
||||
legacySolution,
|
||||
multifileSolution,
|
||||
onlyGithubLink,
|
||||
onlySolution,
|
||||
withChallenges
|
||||
@ -13,10 +14,14 @@ describe('getSolutionDisplayType', () => {
|
||||
expect(getSolutionDisplayType(onlyGithubLink)).toBe('none');
|
||||
});
|
||||
it('should handle legacy solutions', () => {
|
||||
expect(getSolutionDisplayType(legacySolution)).toBe('showFilesSolution');
|
||||
expect(getSolutionDisplayType(legacySolution)).toBe('showUserCode');
|
||||
});
|
||||
it('should handle solutions with files', () => {
|
||||
expect(getSolutionDisplayType(withChallenges)).toBe('showFilesSolution');
|
||||
expect.assertions(2);
|
||||
expect(getSolutionDisplayType(withChallenges)).toBe('showUserCode');
|
||||
expect(getSolutionDisplayType(multifileSolution)).toBe(
|
||||
'showMultifileProjectSolution'
|
||||
);
|
||||
});
|
||||
it('should handle solutions with a single valid url', () => {
|
||||
expect.assertions(2);
|
||||
@ -24,6 +29,6 @@ describe('getSolutionDisplayType', () => {
|
||||
expect(getSolutionDisplayType(invalidGithubLink)).toBe('showProjectLink');
|
||||
});
|
||||
it('should handle solutions with both links', () => {
|
||||
expect(getSolutionDisplayType(bothLinks)).toBe('showProjectAndGitHubLinks');
|
||||
expect(getSolutionDisplayType(bothLinks)).toBe('showProjectAndGithubLinks');
|
||||
});
|
||||
});
|
||||
|
@ -1,16 +1,21 @@
|
||||
import type { CompletedChallenge } from '../redux/prop-types';
|
||||
import { challengeTypes } from '../../utils/challenge-types';
|
||||
import { maybeUrlRE } from '.';
|
||||
|
||||
export const getSolutionDisplayType = ({
|
||||
solution,
|
||||
githubLink,
|
||||
challengeFiles
|
||||
challengeFiles,
|
||||
challengeType
|
||||
}: CompletedChallenge) => {
|
||||
if (challengeFiles?.length) return 'showFilesSolution';
|
||||
if (challengeFiles?.length)
|
||||
return challengeType === challengeTypes.multiFileCertProject
|
||||
? 'showMultifileProjectSolution'
|
||||
: 'showUserCode';
|
||||
if (!solution) return 'none';
|
||||
// Some of the user records still have JavaScript project solutions stored as
|
||||
// solution strings
|
||||
if (!maybeUrlRE.test(solution)) return 'showFilesSolution';
|
||||
if (maybeUrlRE.test(githubLink ?? '')) return 'showProjectAndGitHubLinks';
|
||||
if (!maybeUrlRE.test(solution)) return 'showUserCode';
|
||||
if (maybeUrlRE.test(githubLink ?? '')) return 'showProjectAndGithubLinks';
|
||||
return 'showProjectLink';
|
||||
};
|
||||
|
@ -126,9 +126,7 @@ function getProjectPreviewConfig(challenge, allChallengeEdges) {
|
||||
challengeType !== challengeTypes.multiFileCertProject,
|
||||
challengeData: {
|
||||
challengeType: lastChallenge.challengeType,
|
||||
challengeFiles: projectPreviewChallengeFiles,
|
||||
required: lastChallenge.required,
|
||||
template: lastChallenge.template
|
||||
challengeFiles: projectPreviewChallengeFiles
|
||||
}
|
||||
};
|
||||
}
|
||||
|
18
client/utils/test-utils.jsx
Normal file
18
client/utils/test-utils.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { render as rtlRender } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
function render(ui, store) {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
function Wrapper({ children }) {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
return rtlRender(ui, { wrapper: Wrapper });
|
||||
}
|
||||
|
||||
// re-export everything
|
||||
// eslint-disable-next-line import/export
|
||||
export * from '@testing-library/react';
|
||||
// override render method
|
||||
// eslint-disable-next-line import/export
|
||||
export { render };
|
@ -97,6 +97,19 @@ function setImportedFiles(importedFiles, poly) {
|
||||
return newPoly;
|
||||
}
|
||||
|
||||
// This is currently only used to add back properties that are not stored in the
|
||||
// database.
|
||||
function regeneratePathAndHistory(poly) {
|
||||
const newPath = poly.name + '.' + poly.ext;
|
||||
const newPoly = {
|
||||
...poly,
|
||||
path: newPath,
|
||||
history: [newPath]
|
||||
};
|
||||
checkPoly(newPoly);
|
||||
return newPoly;
|
||||
}
|
||||
|
||||
// clearHeadTail(poly: PolyVinyl) => PolyVinyl
|
||||
function clearHeadTail(poly) {
|
||||
checkPoly(poly);
|
||||
@ -151,6 +164,7 @@ module.exports = {
|
||||
setExt,
|
||||
setImportedFiles,
|
||||
compileHeadTail,
|
||||
regeneratePathAndHistory,
|
||||
transformContents,
|
||||
transformHeadTailAndContents
|
||||
};
|
||||
|
Reference in New Issue
Block a user