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 PropTypes from 'prop-types';
|
||||
import format from 'date-fns/format';
|
||||
import { find, reverse, sortBy } from 'lodash';
|
||||
import { Button, Modal, Table } from '@freecodecamp/react-bootstrap';
|
||||
import { reverse, sortBy } from 'lodash';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Table,
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import { useStaticQuery, graphql } from 'gatsby';
|
||||
|
||||
import './timeline.css';
|
||||
import TimelinePagination from './TimelinePagination';
|
||||
import { FullWidthRow, Link } from '../../helpers';
|
||||
import SolutionViewer from '../../settings/SolutionViewer';
|
||||
@ -13,6 +20,8 @@ import {
|
||||
getPathFromID,
|
||||
getTitleFromId
|
||||
} from '../../../../../utils';
|
||||
|
||||
import { maybeUrlRE } from '../../../utils';
|
||||
import CertificationIcon from '../../../assets/icons/CertificationIcon';
|
||||
|
||||
// Items per page in timeline.
|
||||
@ -66,7 +75,9 @@ class TimelineInner extends Component {
|
||||
this.state = {
|
||||
solutionToView: null,
|
||||
solutionOpen: false,
|
||||
pageNo: 1
|
||||
pageNo: 1,
|
||||
solution: null,
|
||||
files: null
|
||||
};
|
||||
|
||||
this.closeSolution = this.closeSolution.bind(this);
|
||||
@ -76,11 +87,73 @@ class TimelineInner extends Component {
|
||||
this.prevPage = this.prevPage.bind(this);
|
||||
this.nextPage = this.nextPage.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) {
|
||||
const { idToNameMap, username } = this.props;
|
||||
const { id } = completed;
|
||||
const { id, files, githubLink, solution } = completed;
|
||||
const completedDate = new Date(completed.completedDate);
|
||||
const { challengeTitle, challengePath, certPath } = idToNameMap.get(id);
|
||||
return (
|
||||
@ -99,6 +172,7 @@ class TimelineInner extends Component {
|
||||
<Link to={challengePath}>{challengeTitle}</Link>
|
||||
)}
|
||||
</td>
|
||||
<td>{this.renderViewButton(id, files, githubLink, solution)}</td>
|
||||
<td className='text-center'>
|
||||
<time dateTime={completedDate.toISOString()}>
|
||||
{format(completedDate, 'MMMM d, y')}
|
||||
@ -107,11 +181,13 @@ class TimelineInner extends Component {
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
viewSolution(id) {
|
||||
viewSolution(id, solution, files) {
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
solutionToView: id,
|
||||
solutionOpen: true
|
||||
solutionOpen: true,
|
||||
solution,
|
||||
files
|
||||
}));
|
||||
}
|
||||
|
||||
@ -119,7 +195,9 @@ class TimelineInner extends Component {
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
solutionToView: null,
|
||||
solutionOpen: false
|
||||
solutionOpen: false,
|
||||
solution: null,
|
||||
files: null
|
||||
}));
|
||||
}
|
||||
|
||||
@ -169,6 +247,7 @@ class TimelineInner extends Component {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Challenge</th>
|
||||
<th>Solution</th>
|
||||
<th className='text-center'>Completed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -194,10 +273,8 @@ class TimelineInner extends Component {
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<SolutionViewer
|
||||
solution={find(
|
||||
completedMap,
|
||||
({ id: completedId }) => completedId === id
|
||||
)}
|
||||
files={this.state.files}
|
||||
solution={this.state.solution}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<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}
|
||||
bsStyle='primary'
|
||||
className='btn-invert'
|
||||
id={`btn-for-${projectId}`}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Show Code
|
||||
@ -237,6 +238,7 @@ export class CertificationSettings extends Component {
|
||||
bsStyle='primary'
|
||||
className='btn-invert'
|
||||
href={solution}
|
||||
id={`btn-for-${projectId}`}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
@ -249,6 +251,7 @@ export class CertificationSettings extends Component {
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
className='btn-invert'
|
||||
id={`btn-for-${projectId}`}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Show Code
|
||||
|
@ -58,6 +58,46 @@ describe('<certification />', () => {
|
||||
container.querySelector('#button-legacy-front-end')
|
||||
).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 = {
|
||||
@ -207,3 +247,36 @@ const defaultTestProps = {
|
||||
errors: {},
|
||||
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