forked from sombochea/verdaccio-ui
* chore: refactoring version page * refactor: migrate version page to hooks * refactor: Version page better imports * fix: #100 render not found on click item * test: add test for version page * chore: update mocks * test: add scenario for not found package * chore: fix wrong mock path * chore: update mock * chore: add todo list
This commit is contained in:
committed by
GitHub
parent
e7d3c461cd
commit
97e8448098
38
src/App/AppError.tsx
Normal file
38
src/App/AppError.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export interface ErrorProps {
|
||||
children: any;
|
||||
}
|
||||
|
||||
export interface ErrorAppState {
|
||||
hasError: boolean;
|
||||
error: any;
|
||||
info: any;
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends Component<ErrorProps, ErrorAppState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, info: null };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
this.setState({ hasError: true, error, info });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasError, error, info } = this.state;
|
||||
const { children } = this.props;
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<>
|
||||
<h1>{'Something went wrong.'}</h1>
|
||||
<p>{`error: ${error}`}</p>
|
||||
<p>{`info: ${JSON.stringify(info)}`}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ describe('<ActionBar /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -35,7 +35,7 @@ describe('<ActionBar /> component', () => {
|
||||
latest: {},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -57,7 +57,7 @@ describe('<ActionBar /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import HomeIcon from '@material-ui/icons/Home';
|
||||
import List from '@material-ui/core/List';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/Version';
|
||||
import { Fab, ActionListItem } from './styles';
|
||||
import { isURL, extractFileName, downloadFile } from '../../utils/url';
|
||||
import api from '../../utils/api';
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('<Author /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -39,7 +39,7 @@ describe('<Author /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -63,7 +63,7 @@ describe('<Author /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import Avatar from '@material-ui/core/Avatar';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/Version';
|
||||
import { DetailContextConsumer } from '../../pages/Version';
|
||||
import { Heading, AuthorListItem } from './styles';
|
||||
import { isEmail } from '../../utils/url';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { Component, Fragment, ReactElement } from 'react';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/Version';
|
||||
|
||||
import { CardWrap, Heading, Tags, Tag } from './styles';
|
||||
import NoItems from '../NoItems';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component, ReactElement, Fragment } from 'react';
|
||||
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/Version';
|
||||
import Readme from '../Readme';
|
||||
import Versions from '../Versions';
|
||||
import { preventXSS } from '../../utils/sec-utils';
|
||||
@@ -14,6 +14,11 @@ interface DetailContainerState {
|
||||
tabPosition: number;
|
||||
}
|
||||
|
||||
export const README_LABEL = 'Readme';
|
||||
export const DEPS_LABEL = 'Dependencies';
|
||||
export const VERSION_LABEL = 'Versions';
|
||||
export const UPLINKS_LABEL = 'Uplinks';
|
||||
|
||||
class DetailContainer<P> extends Component<P, DetailContainerState> {
|
||||
public state = {
|
||||
tabPosition: 0,
|
||||
@@ -37,10 +42,10 @@ class DetailContainer<P> extends Component<P, DetailContainerState> {
|
||||
private renderListTabs(tabPosition: number): React.ReactElement<HTMLElement> {
|
||||
return (
|
||||
<Tabs indicatorColor={'primary'} onChange={this.handleChange} textColor={'primary'} value={tabPosition} variant={'fullWidth'}>
|
||||
<Tab id={'readme-tab'} label={'Readme'} />
|
||||
<Tab id={'dependencies-tab'} label={'Dependencies'} />
|
||||
<Tab id={'versions-tab'} label={'Versions'} />
|
||||
<Tab id={'uplinks-tab'} label={'Uplinks'} />
|
||||
<Tab data-testid={'readme-tab'} id={'readme-tab'} label={README_LABEL} />
|
||||
<Tab data-testid={'dependencies-tab'} id={'dependencies-tab'} label={DEPS_LABEL} />
|
||||
<Tab data-testid={'versions-tab'} id={'versions-tab'} label={VERSION_LABEL} />
|
||||
<Tab data-testid={'uplinks-tab'} id={'uplinks-tab'} label={UPLINKS_LABEL} />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component, ReactElement } from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
@@ -12,76 +12,52 @@ import Engine from '../Engines/Engines';
|
||||
import Install from '../Install';
|
||||
import Repository from '../Repository/Repository';
|
||||
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
|
||||
import { DetailContext } from '../../pages/Version';
|
||||
|
||||
import { TitleListItem, TitleListItemText } from './styles';
|
||||
|
||||
class DetailSidebar extends Component {
|
||||
public render(): ReactElement<HTMLElement> {
|
||||
return <DetailContextConsumer>{context => this.renderSideBar(context as VersionPageConsumerProps)}</DetailContextConsumer>;
|
||||
}
|
||||
const renderCopyCLI = () => <Install />;
|
||||
const renderMaintainers = () => <Developers type="maintainers" />;
|
||||
const renderContributors = () => <Developers type="contributors" />;
|
||||
const renderRepository = () => <Repository />;
|
||||
const renderAuthor = () => <Author />;
|
||||
const renderEngine = () => <Engine />;
|
||||
const renderDist = () => <Dist />;
|
||||
const renderActionBar = () => <ActionBar />;
|
||||
const renderTitle = (packageName, packageMeta) => {
|
||||
return (
|
||||
<List className="detail-info">
|
||||
<TitleListItem alignItems="flex-start">
|
||||
<TitleListItemText primary={<b>{packageName}</b>} secondary={packageMeta.latest.description} />
|
||||
</TitleListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
private renderSideBar = ({ packageName, packageMeta }): ReactElement<HTMLElement> => {
|
||||
return (
|
||||
<div className={'sidebar-info'}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
{this.renderTitle(packageName, packageMeta)}
|
||||
{this.renderActionBar()}
|
||||
{this.renderCopyCLI()}
|
||||
{this.renderRepository()}
|
||||
{this.renderEngine()}
|
||||
{this.renderDist()}
|
||||
{this.renderAuthor()}
|
||||
{this.renderMaintainers()}
|
||||
{this.renderContributors()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private renderTitle = (packageName, packageMeta) => {
|
||||
return (
|
||||
<List className="detail-info">
|
||||
<TitleListItem alignItems="flex-start">
|
||||
<TitleListItemText primary={<b>{packageName}</b>} secondary={packageMeta.latest.description} />
|
||||
</TitleListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
private renderCopyCLI = () => {
|
||||
return <Install />;
|
||||
};
|
||||
|
||||
private renderMaintainers = () => {
|
||||
return <Developers type="maintainers" />;
|
||||
};
|
||||
|
||||
private renderContributors = () => {
|
||||
return <Developers type="contributors" />;
|
||||
};
|
||||
|
||||
private renderRepository = () => {
|
||||
return <Repository />;
|
||||
};
|
||||
|
||||
private renderAuthor = () => {
|
||||
return <Author />;
|
||||
};
|
||||
|
||||
private renderEngine = () => {
|
||||
return <Engine />;
|
||||
};
|
||||
|
||||
private renderDist = () => {
|
||||
return <Dist />;
|
||||
};
|
||||
|
||||
private renderActionBar = () => {
|
||||
return <ActionBar />;
|
||||
};
|
||||
function renderSideBar(packageName, packageMeta): ReactElement<HTMLElement> {
|
||||
return (
|
||||
<div className={'sidebar-info'}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
{renderTitle(packageName, packageMeta)}
|
||||
{renderActionBar()}
|
||||
{renderCopyCLI()}
|
||||
{renderRepository()}
|
||||
{renderEngine()}
|
||||
{renderDist()}
|
||||
{renderAuthor()}
|
||||
{renderMaintainers()}
|
||||
{renderContributors()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DetailSidebar = () => {
|
||||
const { packageName, packageMeta } = React.useContext(DetailContext);
|
||||
|
||||
return renderSideBar(packageName, packageMeta);
|
||||
};
|
||||
|
||||
export default DetailSidebar;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import Developers, { DevelopersType } from './Developers';
|
||||
import { Fab } from './styles';
|
||||
import { DetailContextProvider } from '../../pages/version/Version';
|
||||
import { DetailContextProvider } from '../../pages/Version';
|
||||
|
||||
describe('test Developers', () => {
|
||||
const packageMeta = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import Add from '@material-ui/icons/Add';
|
||||
|
||||
import { DetailContext } from '../../pages/version/Version';
|
||||
import { DetailContext } from '../../pages/Version';
|
||||
import { AvatarTooltip } from '../AvatarTooltip';
|
||||
import { Details, Heading, Content, Fab } from './styles';
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('<Dist /> component', () => {
|
||||
license: '',
|
||||
},
|
||||
};
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -41,7 +41,7 @@ describe('<Dist /> component', () => {
|
||||
license: 'MIT',
|
||||
},
|
||||
};
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -67,7 +67,7 @@ describe('<Dist /> component', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
|
||||
import List from '@material-ui/core/List';
|
||||
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
|
||||
import { VersionPageConsumerProps, DetailContextConsumer } from '../../pages/Version';
|
||||
import { Heading, DistListItem, DistChips } from './styles';
|
||||
import fileSizeSI from '../../utils/file-size';
|
||||
import { PackageMetaInterface } from 'types/packageMeta';
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('<Engines /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -35,7 +35,7 @@ describe('<Engines /> component', () => {
|
||||
latest: {},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -53,7 +53,7 @@ describe('<Engines /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import Grid from '@material-ui/core/Grid';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
|
||||
import { VersionPageConsumerProps, DetailContextConsumer } from '../../pages/Version';
|
||||
import { Heading, EngineListItem } from './styles';
|
||||
// @ts-ignore
|
||||
import node from './img/node.png';
|
||||
|
||||
@@ -2,8 +2,8 @@ import List from '@material-ui/core/List';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// @ts-ignore
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
|
||||
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/Version';
|
||||
|
||||
import CopyToClipBoard from '../CopyToClipBoard';
|
||||
|
||||
// logos of package managers
|
||||
|
||||
@@ -6,7 +6,7 @@ import Spinner from '../Spinner';
|
||||
import { Wrapper, Badge } from './styles';
|
||||
|
||||
const Loading: React.FC = () => (
|
||||
<Wrapper>
|
||||
<Wrapper data-testid="loading">
|
||||
<Badge>
|
||||
<Logo size={Size.Big} />
|
||||
</Badge>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Loading /> component should render the component in default state 1`] = `"<div class=\\"css-1221txa eimgwje0\\"><div class=\\"css-bxochs eimgwje1\\"><div class=\\"css-ge0nak em793ed0\\"></div></div><div class=\\"css-vqrgi e1ag4h8b0\\"><div class=\\"MuiCircularProgress-root-1 MuiCircularProgress-colorPrimary-4 MuiCircularProgress-indeterminate-3 css-15gl0ho e1ag4h8b1\\" style=\\"width:50px;height:50px\\" role=\\"progressbar\\"><svg class=\\"MuiCircularProgress-svg-6\\" viewBox=\\"22 22 44 44\\"><circle class=\\"MuiCircularProgress-circle-7 MuiCircularProgress-circleIndeterminate-9\\" cx=\\"44\\" cy=\\"44\\" r=\\"20.2\\" fill=\\"none\\" stroke-width=\\"3.6\\"></circle></svg></div></div></div>"`;
|
||||
exports[`<Loading /> component should render the component in default state 1`] = `"<div data-testid=\\"loading\\" class=\\"css-1221txa eimgwje0\\"><div class=\\"css-bxochs eimgwje1\\"><div class=\\"css-ge0nak em793ed0\\"></div></div><div class=\\"css-vqrgi e1ag4h8b0\\"><div class=\\"MuiCircularProgress-root-1 MuiCircularProgress-colorPrimary-4 MuiCircularProgress-indeterminate-3 css-15gl0ho e1ag4h8b1\\" style=\\"width:50px;height:50px\\" role=\\"progressbar\\"><svg class=\\"MuiCircularProgress-svg-6\\" viewBox=\\"22 22 44 44\\"><circle class=\\"MuiCircularProgress-circle-7 MuiCircularProgress-circleIndeterminate-9\\" cx=\\"44\\" cy=\\"44\\" r=\\"20.2\\" fill=\\"none\\" stroke-width=\\"3.6\\"></circle></svg></div></div></div>"`;
|
||||
|
||||
@@ -1,46 +1,43 @@
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import withWidth, { isWidthUp } from '@material-ui/core/withWidth';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import PackageImg from './img/package.svg';
|
||||
import { Card, EmptyPackage, Heading, Inner, List, Wrapper } from './styles';
|
||||
import { Breakpoint } from '@material-ui/core/styles/createBreakpoints';
|
||||
|
||||
export const NOT_FOUND_TEXT = "Sorry, we couldn't find it...";
|
||||
export const NOT_FOUND_TEXT = `Sorry, we couldn't find it...`;
|
||||
export const LABEL_NOT_FOUND = `The page you're looking for doesn't exist.`;
|
||||
export const LABEL_FOOTER_NOT_FOUND = 'Perhaps these links will help find what you are looking for:';
|
||||
|
||||
export type NotFoundProps = RouteComponentProps & { width: Breakpoint; history };
|
||||
|
||||
const NotFound: React.FC<NotFoundProps> = ({ history, width }) => {
|
||||
const handleGoTo = (to: string): (() => void | undefined) => () => {
|
||||
history.push(to);
|
||||
};
|
||||
const HOME_LABEL = 'Home';
|
||||
|
||||
const handleGoBack = (): ((e: React.MouseEvent<HTMLElement, MouseEvent>) => void | undefined) => () => {
|
||||
history.goBack();
|
||||
};
|
||||
const renderSubTitle = (): JSX.Element => (
|
||||
<Typography variant="subtitle1">
|
||||
<div>{LABEL_NOT_FOUND}</div>
|
||||
<div>{LABEL_FOOTER_NOT_FOUND}</div>
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const NotFound: React.FC<NotFoundProps> = ({ history, width }) => {
|
||||
const handleGomHome = useCallback(() => {
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
|
||||
const renderList = (): JSX.Element => (
|
||||
<List>
|
||||
<ListItem button={true} divider={true} onClick={handleGoTo('/')}>
|
||||
{'Home'}
|
||||
</ListItem>
|
||||
<ListItem button={true} divider={true} onClick={handleGoBack()}>
|
||||
{'Back'}
|
||||
<ListItem button={true} divider={true} onClick={handleGomHome}>
|
||||
{HOME_LABEL}
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
|
||||
const renderSubTitle = (): JSX.Element => (
|
||||
<Typography variant="subtitle1">
|
||||
<div>{"The page you're looking for doesn't exist."}</div>
|
||||
<div>{'Perhaps these links will help find what you are looking for:'}</div>
|
||||
</Typography>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Wrapper data-testid="404">
|
||||
<Inner>
|
||||
<EmptyPackage alt="404 - Page not found" src={PackageImg} />
|
||||
<Heading className="not-found-text" variant={isWidthUp('sm', width) ? 'h2' : 'h4'}>
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('<Repository /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -34,7 +34,7 @@ describe('<Repository /> component', () => {
|
||||
latest: {},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
@@ -55,7 +55,7 @@ describe('<Repository /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import Avatar from '@material-ui/core/Avatar';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/Version';
|
||||
import { DetailContextConsumer } from '../../pages/Version';
|
||||
import CopyToClipBoard from '../CopyToClipBoard';
|
||||
|
||||
import { Heading, GithubLink, RepositoryListItem } from './styles';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { ReactElement } from 'react';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/Version';
|
||||
import { DetailContextConsumer } from '../../pages/Version';
|
||||
import NoItems from '../NoItems';
|
||||
import { formatDateDistance } from '../../utils/package';
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('<Version /> component', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('../../pages/version/Version', () => ({
|
||||
jest.doMock('../../pages/Version', () => ({
|
||||
DetailContextConsumer: component => {
|
||||
return component.children({ packageMeta });
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { DetailContextConsumer } from '../../pages/version/Version';
|
||||
import { formatDateDistance } from '../../utils/package';
|
||||
import { Heading, Spacer, ListItemText } from './styles';
|
||||
import React, { ReactElement } from 'react';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/Version';
|
||||
import { formatDateDistance } from '../../utils/package';
|
||||
import { DIST_TAGS } from '../../../lib/constants';
|
||||
import { Heading, Spacer, ListItemText } from './styles';
|
||||
|
||||
const NOT_AVAILABLE = 'Not available';
|
||||
|
||||
|
||||
27
src/pages/Version/Layout.tsx
Normal file
27
src/pages/Version/Layout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { FC, ReactElement } from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import DetailContainer from '../../components/DetailContainer';
|
||||
import DetailSidebar from '../../components/DetailSidebar';
|
||||
|
||||
function renderDetail(): ReactElement<HTMLElement> {
|
||||
return <DetailContainer />;
|
||||
}
|
||||
|
||||
function renderSidebar(): ReactElement<HTMLElement> {
|
||||
return <DetailSidebar />;
|
||||
}
|
||||
|
||||
const Layout: FC<{}> = () => {
|
||||
return (
|
||||
<Grid className={'container content'} container={true} data-testid="version-layout" spacing={0}>
|
||||
<Grid item={true} xs={8}>
|
||||
{renderDetail()}
|
||||
</Grid>
|
||||
<Grid item={true} xs={4}>
|
||||
{renderSidebar()}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export { Layout };
|
||||
96
src/pages/Version/Version.test.tsx
Normal file
96
src/pages/Version/Version.test.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { render, cleanup } from '@testing-library/react';
|
||||
|
||||
import { MemoryRouter } from 'react-router';
|
||||
|
||||
import vueMetadata from '../../../test/fixtures/metadata/vue.json';
|
||||
|
||||
import Version from './Version';
|
||||
import { waitForElement } from '@testing-library/dom';
|
||||
import ErrorBoundary from '../../App/AppError';
|
||||
import { LABEL_NOT_FOUND } from '../../components/NotFound/NotFound';
|
||||
// import { NOT_FOUND_TEXT } from '../../components/NotFound/NotFound';
|
||||
|
||||
// :-) we mock this otherways fails on render, some weird issue on material-ui
|
||||
jest.mock('@material-ui/core/Avatar');
|
||||
|
||||
describe('test Version page', () => {
|
||||
beforeAll(() => {
|
||||
// FIXME: a better way to mock this
|
||||
// @ts-ignore
|
||||
global.window.VERDACCIO_API_URL = 'http://test';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should render the version page', async () => {
|
||||
const readmeText = 'test';
|
||||
// @ts-ignore
|
||||
fetch.mockResponses(
|
||||
[[JSON.stringify(vueMetadata)], { status: 200, headers: { 'content-type': 'application/json' } }],
|
||||
[[`<p align="center">${readmeText}</p>`], { status: 200, headers: { 'content-type': 'text/html' } }]
|
||||
);
|
||||
|
||||
const { getByTestId, getByText } = render(
|
||||
<ErrorBoundary>
|
||||
<MemoryRouter>
|
||||
<Version match={{ params: { ['package']: 'vue' } }}></Version>
|
||||
</MemoryRouter>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
// first it display loading
|
||||
const hasLoading = getByTestId('loading');
|
||||
expect(hasLoading).toBeTruthy();
|
||||
|
||||
// we wait fetch response (mocked above)
|
||||
await waitForElement(() => getByTestId('version-layout'));
|
||||
|
||||
// check whether readme was loaded
|
||||
const hasReadme = getByText(readmeText);
|
||||
|
||||
expect(hasReadme).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should render 404 page if the resources are not found', async () => {
|
||||
// @ts-ignore
|
||||
fetch.mockResponses(
|
||||
[[JSON.stringify({})], { status: 404, headers: { 'content-type': 'application/json' } }],
|
||||
[[``], { status: 404, headers: { 'content-type': 'text/html' } }]
|
||||
);
|
||||
|
||||
const { getByTestId, getByText } = render(
|
||||
<ErrorBoundary>
|
||||
<MemoryRouter>
|
||||
<Version match={{ params: { ['package']: 'vue' } }}></Version>
|
||||
</MemoryRouter>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
// first it display loading
|
||||
const hasLoading = getByTestId('loading');
|
||||
expect(hasLoading).toBeTruthy();
|
||||
|
||||
// we wait fetch response (mocked above)
|
||||
await waitForElement(() => getByTestId('404'));
|
||||
|
||||
// check whether readme was loaded
|
||||
const hasReadme = getByText(LABEL_NOT_FOUND);
|
||||
|
||||
expect(hasReadme).toBeTruthy();
|
||||
});
|
||||
|
||||
// Wanna contribute? Here we some scenarios we need to test
|
||||
|
||||
test.todo('should test click on tabs');
|
||||
test.todo('should check what is rendered int he sidebar is correct');
|
||||
test.todo('should test click back home on 404');
|
||||
test.todo('should test click on elements in the sidebar');
|
||||
test.todo('should test other not consider scenarios');
|
||||
});
|
||||
69
src/pages/Version/Version.tsx
Normal file
69
src/pages/Version/Version.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { callDetailPage, callReadme } from '../../utils/calls';
|
||||
import { buildScopePackage } from '../../utils/package';
|
||||
import Loading from '../../components/Loading/Loading';
|
||||
import NotFound from '../../components/NotFound';
|
||||
|
||||
import { Layout } from './Layout';
|
||||
import { DetailContextProvider } from './context';
|
||||
import { StateInterface } from './types';
|
||||
|
||||
export function getRouterPackageName(params): string {
|
||||
const packageName = params.package;
|
||||
const { scope } = params;
|
||||
if (scope) {
|
||||
return buildScopePackage(scope, packageName);
|
||||
}
|
||||
|
||||
return packageName;
|
||||
}
|
||||
|
||||
const Version = ({ match: { params } }) => {
|
||||
const pkgName = getRouterPackageName(params);
|
||||
const [readMe, setReadme] = useState();
|
||||
const [packageName, setPackageName] = useState(pkgName);
|
||||
const [packageMeta, setPackageMeta] = useState();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const packageMeta = (await callDetailPage(packageName)) as Partial<StateInterface>;
|
||||
const readMe = (await callReadme(packageName)) as Partial<StateInterface>;
|
||||
setReadme(readMe);
|
||||
setPackageMeta(packageMeta);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setNotFound(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [packageName]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Verdaccio - ${packageName}`;
|
||||
}, [packageName]);
|
||||
|
||||
useEffect(() => {
|
||||
const pkgName = getRouterPackageName(params);
|
||||
|
||||
setPackageName(pkgName);
|
||||
}, [params]);
|
||||
|
||||
const isNotFound = notFound || typeof packageMeta === 'undefined' || typeof packageName === 'undefined' || typeof readMe === 'undefined';
|
||||
const renderContent = (): React.ReactElement<HTMLElement> => {
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
} else if (isNotFound) {
|
||||
return <NotFound />;
|
||||
} else {
|
||||
return <Layout />;
|
||||
}
|
||||
};
|
||||
|
||||
return <DetailContextProvider value={{ packageMeta, readMe, packageName, enableLoading: setIsLoading }}>{renderContent()}</DetailContextProvider>;
|
||||
};
|
||||
|
||||
export default Version;
|
||||
7
src/pages/Version/context.ts
Normal file
7
src/pages/Version/context.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import React, { Consumer, Provider } from 'react';
|
||||
import { DetailContextProps, VersionPageConsumerProps } from './types';
|
||||
|
||||
export const DetailContext = React.createContext<Partial<DetailContextProps>>({});
|
||||
|
||||
export const DetailContextProvider: Provider<Partial<VersionPageConsumerProps>> = DetailContext.Provider;
|
||||
export const DetailContextConsumer: Consumer<Partial<VersionPageConsumerProps>> = DetailContext.Consumer;
|
||||
3
src/pages/Version/index.ts
Normal file
3
src/pages/Version/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { DetailContextProps, VersionPageConsumerProps } from './types';
|
||||
export { DetailContext, DetailContextConsumer, DetailContextProvider } from './context';
|
||||
export { default } from './Version';
|
||||
28
src/pages/Version/types.ts
Normal file
28
src/pages/Version/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { PackageMetaInterface } from '../../../types/packageMeta';
|
||||
|
||||
export interface DetailContextProps {
|
||||
packageMeta: PackageMetaInterface;
|
||||
readMe: string;
|
||||
packageName: string;
|
||||
enableLoading: () => void;
|
||||
}
|
||||
|
||||
export interface VersionPageConsumerProps {
|
||||
packageMeta: PackageMetaInterface;
|
||||
readMe: string;
|
||||
packageName: string;
|
||||
// FIXME: looking for the appropiated type here
|
||||
enableLoading: any;
|
||||
}
|
||||
|
||||
export interface PropsInterface {
|
||||
match: boolean;
|
||||
}
|
||||
|
||||
export interface StateInterface {
|
||||
readMe: string;
|
||||
packageName: string;
|
||||
packageMeta?: PackageMetaInterface;
|
||||
isLoading: boolean;
|
||||
notFound: boolean;
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import React, { Component, ReactElement, Consumer, Provider } from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Loading from '../../components/Loading/Loading';
|
||||
import DetailContainer from '../../components/DetailContainer/DetailContainer';
|
||||
import DetailSidebar from '../../components/DetailSidebar/DetailSidebar';
|
||||
import { callDetailPage } from '../../utils/calls';
|
||||
import { getRouterPackageName } from '../../utils/package';
|
||||
import NotFound from '../../components/NotFound';
|
||||
import { PackageMetaInterface } from '../../../types/packageMeta';
|
||||
|
||||
export interface DetailContextProps {
|
||||
packageMeta: PackageMetaInterface;
|
||||
readMe: string;
|
||||
packageName: string;
|
||||
enableLoading: () => void;
|
||||
}
|
||||
|
||||
export const DetailContext = React.createContext<Partial<DetailContextProps>>({});
|
||||
|
||||
export interface VersionPageConsumerProps {
|
||||
packageMeta: PackageMetaInterface;
|
||||
readMe: string;
|
||||
packageName: string;
|
||||
enableLoading: () => void;
|
||||
}
|
||||
|
||||
export const DetailContextProvider: Provider<Partial<VersionPageConsumerProps>> = DetailContext.Provider;
|
||||
export const DetailContextConsumer: Consumer<Partial<VersionPageConsumerProps>> = DetailContext.Consumer;
|
||||
|
||||
interface PropsInterface {
|
||||
match: boolean;
|
||||
}
|
||||
|
||||
interface StateInterface {
|
||||
readMe: string;
|
||||
packageName: string;
|
||||
packageMeta: PackageMetaInterface | null;
|
||||
isLoading: boolean;
|
||||
notFound: boolean;
|
||||
}
|
||||
|
||||
class VersionPage extends Component<PropsInterface, Partial<StateInterface>> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
readMe: '',
|
||||
packageName: getRouterPackageName(props.match),
|
||||
packageMeta: null,
|
||||
isLoading: true,
|
||||
notFound: false,
|
||||
};
|
||||
}
|
||||
|
||||
public static getDerivedStateFromProps(nextProps, prevState): { packageName?: string; isLoading: boolean; notFound?: boolean } | null {
|
||||
const { match } = nextProps;
|
||||
const packageName = getRouterPackageName(match);
|
||||
|
||||
if (packageName !== prevState.packageName) {
|
||||
try {
|
||||
return {
|
||||
packageName,
|
||||
isLoading: false,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
notFound: true,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount(): Promise<void> {
|
||||
await this.loadPackageInfo();
|
||||
}
|
||||
|
||||
/* eslint no-unused-vars: 0 */
|
||||
public async componentDidUpdate(nextProps, prevState: StateInterface): Promise<void> {
|
||||
const { packageName } = this.state;
|
||||
if (packageName !== prevState.packageName) {
|
||||
const { readMe, packageMeta } = (await callDetailPage(packageName)) as Partial<StateInterface>;
|
||||
this.setState({
|
||||
readMe,
|
||||
packageMeta,
|
||||
packageName,
|
||||
notFound: false,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render(): ReactElement<HTMLElement> {
|
||||
const { isLoading, packageMeta, readMe, packageName } = this.state;
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
} else if (!packageMeta) {
|
||||
return <NotFound />;
|
||||
} else {
|
||||
return (
|
||||
<DetailContextProvider value={{ packageMeta, readMe, packageName, enableLoading: this.enableLoading }}>
|
||||
<Grid className={'container content'} container={true} spacing={0}>
|
||||
<Grid item={true} xs={8}>
|
||||
{this.renderDetail()}
|
||||
</Grid>
|
||||
<Grid item={true} xs={4}>
|
||||
{this.renderSidebar()}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DetailContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async loadPackageInfo(): Promise<void> {
|
||||
const { packageName } = this.state;
|
||||
// FIXME: use utility
|
||||
document.title = `Verdaccio - ${packageName}`;
|
||||
|
||||
this.setState({
|
||||
readMe: '',
|
||||
});
|
||||
|
||||
try {
|
||||
const { readMe, packageMeta } = (await callDetailPage(packageName)) as Partial<StateInterface>;
|
||||
this.setState({
|
||||
readMe,
|
||||
packageMeta,
|
||||
packageName,
|
||||
notFound: false,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
notFound: true,
|
||||
packageName,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public enableLoading = () => {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
});
|
||||
};
|
||||
|
||||
public renderDetail(): ReactElement<HTMLElement> {
|
||||
return <DetailContainer />;
|
||||
}
|
||||
|
||||
public renderSidebar(): ReactElement<HTMLElement> {
|
||||
return <DetailSidebar />;
|
||||
}
|
||||
}
|
||||
|
||||
export default VersionPage;
|
||||
@@ -1 +0,0 @@
|
||||
export { default, DetailContextProps } from './Version';
|
||||
@@ -13,7 +13,7 @@ const history = createBrowserHistory({
|
||||
});
|
||||
|
||||
const NotFound = asyncComponent(() => import('./components/NotFound'));
|
||||
const VersionPackage = asyncComponent(() => import('./pages/version/Version'));
|
||||
const VersionPackage = asyncComponent(() => import('./pages/Version'));
|
||||
const HomePage = asyncComponent(() => import('./pages/home'));
|
||||
|
||||
interface RouterAppProps {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import API from './api';
|
||||
import { PackageMetaInterface } from 'types/packageMeta';
|
||||
|
||||
export interface DetailPage {
|
||||
readMe: string | {};
|
||||
packageMeta: PackageMetaInterface | {};
|
||||
export async function callReadme(packageName): Promise<string | {}> {
|
||||
return await API.request<string | {}>(`package/readme/${packageName}`, 'GET');
|
||||
}
|
||||
|
||||
export async function callDetailPage(packageName): Promise<DetailPage> {
|
||||
const readMe = await API.request<string | {}>(`package/readme/${packageName}`, 'GET');
|
||||
export async function callDetailPage(packageName): Promise<PackageMetaInterface | {}> {
|
||||
const packageMeta = await API.request<PackageMetaInterface | {}>(`sidebar/${packageName}`, 'GET');
|
||||
|
||||
return { readMe, packageMeta };
|
||||
return packageMeta;
|
||||
}
|
||||
|
||||
@@ -56,14 +56,8 @@ export function formatDateDistance(lastUpdate): string {
|
||||
return distanceInWordsToNow(new Date(lastUpdate));
|
||||
}
|
||||
|
||||
export function getRouterPackageName(match): string {
|
||||
const packageName = match.params.package;
|
||||
const scope = match.params.scope;
|
||||
if (scope) {
|
||||
return `@${scope}/${packageName}`;
|
||||
}
|
||||
|
||||
return packageName;
|
||||
export function buildScopePackage(scope: string, packageName: string) {
|
||||
return `@${scope}/${packageName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user