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:
committed by
GitHub
parent
fdcf657d93
commit
56ad1c7d60
@ -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>
|
||||||
|
114
client/src/components/profile/components/TimeLine.test.js
Normal file
114
client/src/components/profile/components/TimeLine.test.js
Normal 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'
|
||||||
|
};
|
3
client/src/components/profile/components/timeline.css
Normal file
3
client/src/components/profile/components/timeline.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.timeline-row .solutions-dropdown .dropdown {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user