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