feat(client): add show solution button in TimeLine (#40120)

* feat: add show solution button in TimeLine

* feat: added tests for CertificateSettings and TimeLine

* feat: view button only for projects

* feat: view button visible only for projects
This commit is contained in:
dipayanDebTheCoder
2020-11-04 20:10:56 +05:30
committed by GitHub
parent fdcf657d93
commit 56ad1c7d60
5 changed files with 281 additions and 11 deletions

View File

@ -1,10 +1,17 @@
import React, { Component, useMemo } from 'react'; import React, { Component, useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { find, reverse, sortBy } from 'lodash'; import { reverse, sortBy } from 'lodash';
import { Button, Modal, Table } from '@freecodecamp/react-bootstrap'; import {
Button,
Modal,
Table,
DropdownButton,
MenuItem
} from '@freecodecamp/react-bootstrap';
import { useStaticQuery, graphql } from 'gatsby'; import { useStaticQuery, graphql } from 'gatsby';
import './timeline.css';
import TimelinePagination from './TimelinePagination'; import TimelinePagination from './TimelinePagination';
import { FullWidthRow, Link } from '../../helpers'; import { FullWidthRow, Link } from '../../helpers';
import SolutionViewer from '../../settings/SolutionViewer'; import SolutionViewer from '../../settings/SolutionViewer';
@ -13,6 +20,8 @@ import {
getPathFromID, getPathFromID,
getTitleFromId getTitleFromId
} from '../../../../../utils'; } from '../../../../../utils';
import { maybeUrlRE } from '../../../utils';
import CertificationIcon from '../../../assets/icons/CertificationIcon'; import CertificationIcon from '../../../assets/icons/CertificationIcon';
// Items per page in timeline. // Items per page in timeline.
@ -66,7 +75,9 @@ class TimelineInner extends Component {
this.state = { this.state = {
solutionToView: null, solutionToView: null,
solutionOpen: false, solutionOpen: false,
pageNo: 1 pageNo: 1,
solution: null,
files: null
}; };
this.closeSolution = this.closeSolution.bind(this); this.closeSolution = this.closeSolution.bind(this);
@ -76,11 +87,73 @@ class TimelineInner extends Component {
this.prevPage = this.prevPage.bind(this); this.prevPage = this.prevPage.bind(this);
this.nextPage = this.nextPage.bind(this); this.nextPage = this.nextPage.bind(this);
this.lastPage = this.lastPage.bind(this); this.lastPage = this.lastPage.bind(this);
this.renderViewButton = this.renderViewButton.bind(this);
}
renderViewButton(id, files, githubLink, solution) {
if (files && files.length) {
return (
<Button
block={true}
bsStyle='primary'
className='btn-invert'
id={`btn-for-${id}`}
onClick={() => this.viewSolution(id, solution, files)}
>
Show Code
</Button>
);
} else if (githubLink) {
return (
<div className='solutions-dropdown'>
<DropdownButton
block={true}
bsStyle='primary'
className='btn-invert'
id={`dropdown-for-${id}`}
title='View'
>
<MenuItem
bsStyle='primary'
href={solution}
rel='noopener noreferrer'
target='_blank'
>
Front End
</MenuItem>
<MenuItem
bsStyle='primary'
href={githubLink}
rel='noopener noreferrer'
target='_blank'
>
Back End
</MenuItem>
</DropdownButton>
</div>
);
} else if (maybeUrlRE.test(solution)) {
return (
<Button
block={true}
bsStyle='primary'
className='btn-invert'
href={solution}
id={`btn-for-${id}`}
rel='noopener noreferrer'
target='_blank'
>
View
</Button>
);
} else {
return null;
}
} }
renderCompletion(completed) { renderCompletion(completed) {
const { idToNameMap, username } = this.props; const { idToNameMap, username } = this.props;
const { id } = completed; const { id, files, githubLink, solution } = completed;
const completedDate = new Date(completed.completedDate); const completedDate = new Date(completed.completedDate);
const { challengeTitle, challengePath, certPath } = idToNameMap.get(id); const { challengeTitle, challengePath, certPath } = idToNameMap.get(id);
return ( return (
@ -99,6 +172,7 @@ class TimelineInner extends Component {
<Link to={challengePath}>{challengeTitle}</Link> <Link to={challengePath}>{challengeTitle}</Link>
)} )}
</td> </td>
<td>{this.renderViewButton(id, files, githubLink, solution)}</td>
<td className='text-center'> <td className='text-center'>
<time dateTime={completedDate.toISOString()}> <time dateTime={completedDate.toISOString()}>
{format(completedDate, 'MMMM d, y')} {format(completedDate, 'MMMM d, y')}
@ -107,11 +181,13 @@ class TimelineInner extends Component {
</tr> </tr>
); );
} }
viewSolution(id) { viewSolution(id, solution, files) {
this.setState(state => ({ this.setState(state => ({
...state, ...state,
solutionToView: id, solutionToView: id,
solutionOpen: true solutionOpen: true,
solution,
files
})); }));
} }
@ -119,7 +195,9 @@ class TimelineInner extends Component {
this.setState(state => ({ this.setState(state => ({
...state, ...state,
solutionToView: null, solutionToView: null,
solutionOpen: false solutionOpen: false,
solution: null,
files: null
})); }));
} }
@ -169,6 +247,7 @@ class TimelineInner extends Component {
<thead> <thead>
<tr> <tr>
<th>Challenge</th> <th>Challenge</th>
<th>Solution</th>
<th className='text-center'>Completed</th> <th className='text-center'>Completed</th>
</tr> </tr>
</thead> </thead>
@ -194,10 +273,8 @@ class TimelineInner extends Component {
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<SolutionViewer <SolutionViewer
solution={find( files={this.state.files}
completedMap, solution={this.state.solution}
({ id: completedId }) => completedId === id
)}
/> />
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>

View File

@ -0,0 +1,114 @@
/* global expect */
import React from 'react';
import { render } from '@testing-library/react';
import TimeLine from './TimeLine';
import { useStaticQuery } from 'gatsby';
beforeEach(() => {
useStaticQuery.mockImplementationOnce(() => ({
allChallengeNode: {
edges: [
{
node: {
fields: {
slug: ''
},
id: '5e46f802ac417301a38fb92b',
title: 'Page View Time Series Visualizer'
}
},
{
node: {
fields: {
slug: ''
},
id: '5e4f5c4b570f7e3a4949899f',
title: 'Sea Level Predictor'
}
},
{
node: {
fields: {
slug: ''
},
id: '5e46f7f8ac417301a38fb92a',
title: 'Medical Data Visualizer'
}
}
]
}
}));
});
describe('<TimeLine />', () => {
it('Render button when only solution is present', () => {
const { container } = render(<TimeLine {...propsForOnlySolution} />);
expect(
container.querySelector('#btn-for-5e46f802ac417301a38fb92b')
).toHaveAttribute('href', 'https://github.com/freeCodeCamp/freeCodeCamp');
});
it('Render button when both githubLink and solution is present', () => {
const { container } = render(<TimeLine {...propsForOnlySolution} />);
const linkList = container.querySelector(
'#dropdown-for-5e4f5c4b570f7e3a4949899f + ul'
);
const links = linkList.querySelectorAll('a');
expect(links[0]).toHaveAttribute(
'href',
'https://github.com/freeCodeCamp/freeCodeCamp1'
);
expect(links[1]).toHaveAttribute(
'href',
'https://github.com/freeCodeCamp/freeCodeCamp2'
);
});
it('rendering the correct button when files is present', () => {
const { getByText } = render(<TimeLine {...propsForOnlySolution} />);
const button = getByText('Show Code');
expect(button).toBeInTheDocument();
});
});
const contents = 'This is not JS';
const ext = 'js';
const key = 'indexjs';
const name = 'index';
const path = 'index.js';
const propsForOnlySolution = {
completedMap: [
{
id: '5e46f802ac417301a38fb92b',
solution: 'https://github.com/freeCodeCamp/freeCodeCamp',
completedDate: 1604311988825
},
{
id: '5e4f5c4b570f7e3a4949899f',
solution: 'https://github.com/freeCodeCamp/freeCodeCamp1',
githubLink: 'https://github.com/freeCodeCamp/freeCodeCamp2',
completedDate: 1604311988828
},
{
id: '5e46f7f8ac417301a38fb92a',
completedDate: 1604043678032,
files: [
{
contents,
ext,
key,
name,
path
}
]
}
],
username: 'developmentuser'
};

View File

@ -0,0 +1,3 @@
.timeline-row .solutions-dropdown .dropdown {
width: 100%;
}

View File

@ -194,6 +194,7 @@ export class CertificationSettings extends Component {
block={true} block={true}
bsStyle='primary' bsStyle='primary'
className='btn-invert' className='btn-invert'
id={`btn-for-${projectId}`}
onClick={onClickHandler} onClick={onClickHandler}
> >
Show Code Show Code
@ -237,6 +238,7 @@ export class CertificationSettings extends Component {
bsStyle='primary' bsStyle='primary'
className='btn-invert' className='btn-invert'
href={solution} href={solution}
id={`btn-for-${projectId}`}
rel='noopener noreferrer' rel='noopener noreferrer'
target='_blank' target='_blank'
> >
@ -249,6 +251,7 @@ export class CertificationSettings extends Component {
block={true} block={true}
bsStyle='primary' bsStyle='primary'
className='btn-invert' className='btn-invert'
id={`btn-for-${projectId}`}
onClick={onClickHandler} onClick={onClickHandler}
> >
Show Code Show Code

View File

@ -58,6 +58,46 @@ describe('<certification />', () => {
container.querySelector('#button-legacy-front-end') container.querySelector('#button-legacy-front-end')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('Render button when only solution is present', () => {
const { container } = renderWithRedux(
<CertificationSettings {...propsForOnlySolution} />
);
expect(
container.querySelector('#btn-for-5e46f802ac417301a38fb92b')
).toHaveAttribute('href', 'https://github.com/freeCodeCamp/freeCodeCamp');
});
it('Render button when both githubLink and solution is present', () => {
const { container } = renderWithRedux(
<CertificationSettings {...propsForOnlySolution} />
);
const linkList = container.querySelector(
'#dropdown-for-5e4f5c4b570f7e3a4949899f + ul'
);
const links = linkList.querySelectorAll('a');
expect(links[0]).toHaveAttribute(
'href',
'https://github.com/freeCodeCamp/freeCodeCamp1'
);
expect(links[1]).toHaveAttribute(
'href',
'https://github.com/freeCodeCamp/freeCodeCamp2'
);
});
it('rendering the correct button when files is present', () => {
const { getByText } = renderWithRedux(
<CertificationSettings {...propsForOnlySolution} />
);
const button = getByText('Show Code');
expect(button).toBeInTheDocument();
});
}); });
const defaultTestProps = { const defaultTestProps = {
@ -207,3 +247,36 @@ const defaultTestProps = {
errors: {}, errors: {},
submit: () => {} submit: () => {}
}; };
const contents = 'This is not JS';
const ext = 'js';
const key = 'indexjs';
const name = 'index';
const path = 'index.js';
const propsForOnlySolution = {
...defaultTestProps,
completedChallenges: [
{
id: '5e46f802ac417301a38fb92b',
solution: 'https://github.com/freeCodeCamp/freeCodeCamp'
},
{
id: '5e4f5c4b570f7e3a4949899f',
solution: 'https://github.com/freeCodeCamp/freeCodeCamp1',
githubLink: 'https://github.com/freeCodeCamp/freeCodeCamp2'
},
{
id: '5e46f7f8ac417301a38fb92a',
files: [
{
contents,
ext,
key,
name,
path
}
]
}
]
};