ActionBar Component - Replaced class by func. comp (#330)

This commit is contained in:
Priscila Oliveira 2019-12-03 09:44:44 +01:00 committed by GitHub
parent fcad6fa794
commit 742971db0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 297 additions and 218 deletions

View File

@ -1,89 +1,71 @@
import React from 'react';
import { mount } from '../../utils/test-enzyme';
import api from '../../utils/api';
import { render, cleanup } from '../../utils/test-react-testing-library';
import { DetailContext, DetailContextProps } from '../../pages/Version';
import { ActionBar } from './ActionBar';
import ActionBar from './ActionBar';
const mockPackageMeta: jest.Mock = jest.fn(() => ({
const detailContextValue: DetailContextProps = {
packageName: 'foo',
readMe: 'test',
enableLoading: () => {},
isLoading: false,
hasNotBeenFound: false,
packageMeta: {
_uplinks: {},
latest: {
homepage: 'https://verdaccio.tld',
name: '@verdaccio/local-storage',
version: '8.0.1-next.1',
dist: { fileCount: 0, unpackedSize: 0, tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz' },
homepage: 'https://verdaccio.org',
bugs: {
url: 'https://verdaccio.tld/bugs',
},
dist: {
tarball: 'https://verdaccio.tld/download',
url: 'https://github.com/verdaccio/monorepo/issues',
},
},
}));
},
};
jest.mock('../../pages/Version', () => ({
DetailContextConsumer: component => {
return component.children({ packageMeta: mockPackageMeta() });
},
}));
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({ contextValue }) => (
<DetailContext.Provider value={contextValue}>
<ActionBar />
</DetailContext.Provider>
);
describe('<ActionBar /> component', () => {
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
afterEach(() => {
cleanup();
});
test('should render the component in default state', () => {
const wrapper = mount(<ActionBar />);
expect(wrapper.html()).toMatchSnapshot();
const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(container.firstChild).toMatchSnapshot();
});
test('when there is no action bar data', () => {
mockPackageMeta.mockImplementation(() => ({
latest: {},
}));
const packageMeta = {
...detailContextValue.packageMeta,
latest: {
...detailContextValue.packageMeta.latest,
homepage: undefined,
bugs: undefined,
dist: {
...detailContextValue.packageMeta.latest.dist,
tarball: undefined,
},
},
};
const wrapper = mount(<ActionBar />);
// FIXME: this only renders the DetailContextConsumer, thus
// the wrapper will be always empty
expect(wrapper.html()).toEqual('');
});
test('when there is no latest property in package meta', () => {
mockPackageMeta.mockImplementation(() => ({}));
const wrapper = mount(<ActionBar />);
expect(wrapper.html()).toEqual('');
const { container } = render(<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />);
expect(container.firstChild).toMatchSnapshot();
});
test('when there is a button to download a tarball', () => {
mockPackageMeta.mockImplementation(() => ({
latest: {
dist: {
tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
},
},
}));
const wrapper = mount(<ActionBar />);
expect(wrapper.html()).toMatchSnapshot();
const button = wrapper.find('button');
expect(button).toHaveLength(1);
const spy = jest.spyOn(api, 'request');
button.simulate('click');
expect(spy).toHaveBeenCalled();
const { getByTitle } = render(<ComponentToBeRendered contextValue={{ ...detailContextValue }} />);
expect(getByTitle('Download tarball')).toBeTruthy();
});
test('when there is a button to open an issue', () => {
mockPackageMeta.mockImplementation(() => ({
latest: {
bugs: {
url: 'https://verdaccio.tld/bugs',
},
},
}));
const wrapper = mount(<ActionBar />);
expect(wrapper.html()).toMatchSnapshot();
const button = wrapper.find('button');
expect(button).toHaveLength(1);
const { getByTitle } = render(<ComponentToBeRendered contextValue={{ ...detailContextValue }} />);
expect(getByTitle('Open an issue')).toBeTruthy();
});
});

View File

@ -1,133 +1,44 @@
import React, { Component, ReactElement } from 'react';
import BugReportIcon from '@material-ui/icons/BugReport';
import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import React from 'react';
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/Version';
import { isURL, extractFileName, downloadFile } from '../../utils/url';
import api from '../../utils/api';
import Tooltip from '../../muiComponents/Tooltip';
import List from '../../muiComponents/List';
import { DetailContext } from '../../pages/Version';
import { isURL } from '../../utils/url';
import Box from '../../muiComponents/Box';
import { Fab, ActionListItem } from './styles';
import ActionBarAction, { ActionBarActionProps } from './ActionBarAction';
export interface Action {
icon: string;
title: string;
handler?: Function;
}
/* eslint-disable verdaccio/jsx-spread */
const ActionBar: React.FC = () => {
const detailContext = React.useContext(DetailContext);
export async function downloadHandler(link: string): Promise<void> {
const fileStream: Blob = await api.request(link, 'GET', {
headers: {
['accept']:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
},
credentials: 'include',
});
const fileName = extractFileName(link);
downloadFile(fileStream, fileName);
}
const { packageMeta } = detailContext;
const ACTIONS = {
homepage: {
icon: <HomeIcon />,
title: 'Visit homepage',
},
issue: {
icon: <BugReportIcon />,
title: 'Open an issue',
},
tarball: {
icon: <DownloadIcon />,
title: 'Download tarball',
handler: downloadHandler,
},
};
class ActionBar extends Component {
public render(): ReactElement<HTMLElement> {
return (
<DetailContextConsumer>
{context => {
const { packageMeta } = context;
if (!packageMeta) {
if (!packageMeta?.latest) {
return null;
}
return this.renderActionBar(context as VersionPageConsumerProps);
}}
</DetailContextConsumer>
);
const { homepage, bugs, dist } = packageMeta.latest;
const actions: Array<ActionBarActionProps> = [];
if (homepage && isURL(homepage)) {
actions.push({ type: 'VISIT_HOMEPAGE', link: homepage });
}
if (bugs?.url && isURL(bugs.url)) {
actions.push({ type: 'OPEN_AN_ISSUE', link: bugs.url });
}
if (dist?.tarball && isURL(dist.tarball)) {
actions.push({ type: 'DOWNLOAD_TARBALL', link: dist.tarball });
}
private renderIconsWithLink(link: string, component: JSX.Element): ReactElement<HTMLElement> {
return (
<a href={link} target={'_blank'}>
{component}
</a>
<Box alignItems="center" display="flex" marginBottom="8px">
{actions.map(action => (
<ActionBarAction key={action.link} {...action} />
))}
</Box>
);
}
private renderActionBar = ({ packageMeta }) => {
const { latest } = packageMeta;
if (!latest) {
return null;
}
const { homepage, bugs, dist } = latest;
const actionsMap = {
homepage,
issue: bugs ? bugs.url : null,
tarball: dist ? dist.tarball : null,
};
const renderList = Object.keys(actionsMap).reduce((component: React.ReactElement[], value, key) => {
const link = actionsMap[value];
if (link && isURL(link)) {
const actionItem: Action = ACTIONS[value];
if (actionItem.handler) {
const fab = (
<Tooltip key={key} title={actionItem['title']}>
<Fab
/* eslint-disable react/jsx-no-bind */
onClick={() => {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
actionItem.handler!(link);
}}
size={'small'}>
{actionItem['icon']}
</Fab>
</Tooltip>
);
component.push(fab);
} else {
const fab = <Fab size={'small'}>{actionItem['icon']}</Fab>;
component.push(
<Tooltip key={key} title={actionItem['title']}>
<>{this.renderIconsWithLink(link, fab)}</>
</Tooltip>
);
}
}
return component;
}, []);
if (renderList.length > 0) {
return (
<List>
<ActionListItem alignItems={'flex-start'} button={true}>
{renderList}
</ActionListItem>
</List>
);
}
return null;
};
}
export { ActionBar };
export default ActionBar;

View File

@ -0,0 +1,62 @@
import React from 'react';
import styled from '@emotion/styled';
import BugReportIcon from '@material-ui/icons/BugReport';
import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import Tooltip from '../../muiComponents/Tooltip';
import Link from '../Link';
import FloatingActionButton from '../../muiComponents/FloatingActionButton';
import { Theme } from '../../design-tokens/theme';
import downloadTarball from './download-tarball';
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>(props => ({
backgroundColor: props.theme && props.theme.palette.primary.main,
color: props.theme && props.theme.palette.white,
marginRight: 10,
}));
type ActionType = 'VISIT_HOMEPAGE' | 'OPEN_AN_ISSUE' | 'DOWNLOAD_TARBALL';
export interface ActionBarActionProps {
type: ActionType;
link: string;
}
/* eslint-disable react/jsx-no-bind */
/* eslint-disable react/jsx-max-depth */
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
switch (type) {
case 'VISIT_HOMEPAGE':
return (
<Tooltip title="Visit homepage">
<Link external={true} to={link}>
<Fab size="small">
<HomeIcon />
</Fab>
</Link>
</Tooltip>
);
case 'OPEN_AN_ISSUE':
return (
<Tooltip title="Open an issue">
<Link external={true} to={link}>
<Fab size="small">
<BugReportIcon />
</Fab>
</Link>
</Tooltip>
);
case 'DOWNLOAD_TARBALL':
return (
<Tooltip title="Download tarball">
<Fab data-testid="download-tarball-btn" onClick={downloadTarball(link)} size="small">
<DownloadIcon />
</Fab>
</Tooltip>
);
}
};
export default ActionBarAction;

View File

@ -1,7 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ActionBar /> component should render the component in default state 1`] = `""`;
exports[`<ActionBar /> component should render the component in default state 1`] = `
.emotion-0 {
background-color: #4b5e40;
color: #fff;
margin-right: 10px;
}
exports[`<ActionBar /> component when there is a button to download a tarball 1`] = `"<ul class=\\"MuiList-root MuiList-padding\\"><div class=\\"MuiButtonBase-root MuiListItem-root css-l3mdff-ActionListItem eux6shq0 MuiListItem-gutters MuiListItem-button MuiListItem-alignItemsFlexStart\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><button class=\\"MuiButtonBase-root MuiFab-root css-is03ew-Fab eux6shq1 MuiFab-sizeSmall\\" tabindex=\\"0\\" type=\\"button\\" title=\\"Download tarball\\"><span class=\\"MuiFab-label\\"><svg class=\\"MuiSvgIcon-root\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z\\"></path></svg></span><span class=\\"MuiTouchRipple-root\\"></span></button><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
<div
class="MuiBox-root MuiBox-root-2"
>
<a
class=""
href="https://verdaccio.org"
rel="noopener noreferrer"
target="_blank"
title="Visit homepage"
>
<h6
class="MuiTypography-root MuiTypography-subtitle1"
>
<button
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
tabindex="0"
type="button"
>
<span
class="MuiFab-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</h6>
</a>
<a
class=""
href="https://github.com/verdaccio/monorepo/issues"
rel="noopener noreferrer"
target="_blank"
title="Open an issue"
>
<h6
class="MuiTypography-root MuiTypography-subtitle1"
>
<button
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
tabindex="0"
type="button"
>
<span
class="MuiFab-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</h6>
</a>
<button
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
data-testid="download-tarball-btn"
tabindex="0"
title="Download tarball"
type="button"
>
<span
class="MuiFab-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
`;
exports[`<ActionBar /> component when there is a button to open an issue 1`] = `"<ul class=\\"MuiList-root MuiList-padding\\"><div class=\\"MuiButtonBase-root MuiListItem-root css-l3mdff-ActionListItem eux6shq0 MuiListItem-gutters MuiListItem-button MuiListItem-alignItemsFlexStart\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><a href=\\"https://verdaccio.tld/bugs\\" target=\\"_blank\\"><button class=\\"MuiButtonBase-root MuiFab-root css-is03ew-Fab eux6shq1 MuiFab-sizeSmall\\" tabindex=\\"0\\" type=\\"button\\"><span class=\\"MuiFab-label\\"><svg class=\\"MuiSvgIcon-root\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z\\"></path></svg></span><span class=\\"MuiTouchRipple-root\\"></span></button></a><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<ActionBar /> component when there is no action bar data 1`] = `
<div
class="MuiBox-root MuiBox-root-74"
/>
`;

View File

@ -0,0 +1,18 @@
import api from '../../utils/api';
import { extractFileName, downloadFile } from '../../utils/url';
function downloadTarball(link: string) {
return async function downloadHandler(): Promise<void> {
const fileStream: Blob = await api.request(link, 'GET', {
headers: {
['accept']:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
},
credentials: 'include',
});
const fileName = extractFileName(link);
downloadFile(fileStream, fileName);
};
}
export default downloadTarball;

View File

@ -1 +1,2 @@
export { default } from './ActionBar';
export { default as downloadTarball } from './download-tarball';

View File

@ -1,17 +0,0 @@
import styled from '@emotion/styled';
import ListItem from '../../muiComponents/ListItem';
import FloatingActionButton from '../../muiComponents/FloatingActionButton';
import { Theme } from '../../design-tokens/theme';
export const ActionListItem = styled(ListItem)({
paddingTop: 0,
paddingLeft: 0,
paddingRight: 0,
});
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>(props => ({
backgroundColor: props.theme && props.theme.palette.primary.main,
color: props.theme && props.theme.palette.white,
marginRight: '10px',
}));

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react';
import { ActionBar } from '../ActionBar/ActionBar';
import ActionBar from '../ActionBar';
import Author from '../Author';
import Developers from '../Developers';
import Dist from '../Dist/Dist';

View File

@ -7,20 +7,26 @@ interface Props extends Pick<TextProps, 'variant'> {
external?: boolean;
className?: string;
to: string;
children?: React.ReactNode;
}
type LinkRef = HTMLAnchorElement;
/* eslint-disable verdaccio/jsx-spread */
const Link: React.FC<Props> = ({ external, to, children, variant, className, ...props }) => {
const Link = React.forwardRef<LinkRef, Props>(function Link(
{ external, to, children, variant, className, ...props },
ref
) {
const LinkTextContent = <Text variant={variant}>{children}</Text>;
return external ? (
<a className={className} href={to} rel="noopener noreferrer" target="_blank" {...props}>
<a className={className} href={to} ref={ref} rel="noopener noreferrer" target="_blank" {...props}>
{LinkTextContent}
</a>
) : (
<RouterLink className={className} to={to} {...props}>
<RouterLink className={className} innerRef={ref} to={to} {...props}>
{LinkTextContent}
</RouterLink>
);
};
});
export default Link;

View File

@ -9,7 +9,7 @@ import fileSizeSI from '../../utils/file-size';
import { formatDate, formatDateDistance } from '../../utils/package';
import Tooltip from '../../muiComponents/Tooltip';
import { isURL } from '../../utils/url';
import { downloadHandler } from '../ActionBar/ActionBar';
import { downloadTarball } from '../ActionBar';
import ListItem from '../../muiComponents/ListItem';
import Grid from '../../muiComponents/Grid';
@ -140,7 +140,7 @@ const Package: React.FC<PackageInterface> = ({
dist.tarball &&
isURL(dist.tarball) && (
// eslint-disable-next-line
<a onClick={() => downloadHandler(dist.tarball.replace(`https://registry.npmjs.org/`, window.location.href))} target={'_blank'}>
<a onClick={downloadTarball(dist.tarball.replace(`https://registry.npmjs.org/`, window.location.href))} target={'_blank'}>
<Tooltip aria-label={'Download the tar file'} title={'Download tarball'}>
<IconButton aria-label={'Download'}>
{/* eslint-disable-next-line react/jsx-max-depth */}

View File

@ -8,6 +8,7 @@ export interface PackageMetaInterface {
dist: {
fileCount: number;
unpackedSize: number;
tarball?: string;
};
engines?: {
node?: string;
@ -15,6 +16,10 @@ export interface PackageMetaInterface {
};
license?: Partial<LicenseInterface> | string;
version: string;
homepage?: string;
bugs?: {
url: string;
};
repository?: {
type?: string;
url?: string;