diff --git a/src/components/ActionBar/ActionBar.test.tsx b/src/components/ActionBar/ActionBar.test.tsx index 31ed136..daf8b0a 100644 --- a/src/components/ActionBar/ActionBar.test.tsx +++ b/src/components/ActionBar/ActionBar.test.tsx @@ -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(() => ({ - latest: { - homepage: 'https://verdaccio.tld', - bugs: { - url: 'https://verdaccio.tld/bugs', - }, - dist: { - tarball: 'https://verdaccio.tld/download', +const detailContextValue: DetailContextProps = { + packageName: 'foo', + readMe: 'test', + enableLoading: () => {}, + isLoading: false, + hasNotBeenFound: false, + packageMeta: { + _uplinks: {}, + latest: { + 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://github.com/verdaccio/monorepo/issues', + }, }, }, -})); +}; -jest.mock('../../pages/Version', () => ({ - DetailContextConsumer: component => { - return component.children({ packageMeta: mockPackageMeta() }); - }, -})); +const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({ contextValue }) => ( + + + +); describe(' component', () => { - beforeEach(() => { - jest.resetModules(); - jest.resetAllMocks(); + afterEach(() => { + cleanup(); }); test('should render the component in default state', () => { - const wrapper = mount(); - expect(wrapper.html()).toMatchSnapshot(); + const { container } = render(); + 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(); - // 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(); - expect(wrapper.html()).toEqual(''); + const { container } = render(); + 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(); - 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(); + 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(); - expect(wrapper.html()).toMatchSnapshot(); - - const button = wrapper.find('button'); - expect(button).toHaveLength(1); + const { getByTitle } = render(); + expect(getByTitle('Open an issue')).toBeTruthy(); }); }); diff --git a/src/components/ActionBar/ActionBar.tsx b/src/components/ActionBar/ActionBar.tsx index 6a731e4..700c225 100644 --- a/src/components/ActionBar/ActionBar.tsx +++ b/src/components/ActionBar/ActionBar.tsx @@ -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 { - 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: , - title: 'Visit homepage', - }, - issue: { - icon: , - title: 'Open an issue', - }, - tarball: { - icon: , - title: 'Download tarball', - handler: downloadHandler, - }, + if (!packageMeta?.latest) { + return null; + } + + const { homepage, bugs, dist } = packageMeta.latest; + + const actions: Array = []; + + 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 }); + } + + return ( + + {actions.map(action => ( + + ))} + + ); }; -class ActionBar extends Component { - public render(): ReactElement { - return ( - - {context => { - const { packageMeta } = context; - - if (!packageMeta) { - return null; - } - - return this.renderActionBar(context as VersionPageConsumerProps); - }} - - ); - } - - private renderIconsWithLink(link: string, component: JSX.Element): ReactElement { - return ( - - {component} - - ); - } - - 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 = ( - - { - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - actionItem.handler!(link); - }} - size={'small'}> - {actionItem['icon']} - - - ); - component.push(fab); - } else { - const fab = {actionItem['icon']}; - component.push( - - <>{this.renderIconsWithLink(link, fab)} - - ); - } - } - return component; - }, []); - - if (renderList.length > 0) { - return ( - - - {renderList} - - - ); - } - - return null; - }; -} - -export { ActionBar }; +export default ActionBar; diff --git a/src/components/ActionBar/ActionBarAction.tsx b/src/components/ActionBar/ActionBarAction.tsx new file mode 100644 index 0000000..9fb6fec --- /dev/null +++ b/src/components/ActionBar/ActionBarAction.tsx @@ -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 = ({ type, link }) => { + switch (type) { + case 'VISIT_HOMEPAGE': + return ( + + + + + + + + ); + case 'OPEN_AN_ISSUE': + return ( + + + + + + + + ); + case 'DOWNLOAD_TARBALL': + return ( + + + + + + ); + } +}; + +export default ActionBarAction; diff --git a/src/components/ActionBar/__snapshots__/ActionBar.test.tsx.snap b/src/components/ActionBar/__snapshots__/ActionBar.test.tsx.snap index cb1abdf..52b26ca 100644 --- a/src/components/ActionBar/__snapshots__/ActionBar.test.tsx.snap +++ b/src/components/ActionBar/__snapshots__/ActionBar.test.tsx.snap @@ -1,7 +1,118 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` component should render the component in default state 1`] = `""`; +exports[` component should render the component in default state 1`] = ` +.emotion-0 { + background-color: #4b5e40; + color: #fff; + margin-right: 10px; +} -exports[` component when there is a button to download a tarball 1`] = `"
"`; + +`; -exports[` component when there is a button to open an issue 1`] = `"
"`; +exports[` component when there is no action bar data 1`] = ` +
+`; diff --git a/src/components/ActionBar/download-tarball.ts b/src/components/ActionBar/download-tarball.ts new file mode 100644 index 0000000..ab6384f --- /dev/null +++ b/src/components/ActionBar/download-tarball.ts @@ -0,0 +1,18 @@ +import api from '../../utils/api'; +import { extractFileName, downloadFile } from '../../utils/url'; + +function downloadTarball(link: string) { + return async function downloadHandler(): Promise { + 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; diff --git a/src/components/ActionBar/index.ts b/src/components/ActionBar/index.ts index d08aed0..518dc24 100644 --- a/src/components/ActionBar/index.ts +++ b/src/components/ActionBar/index.ts @@ -1 +1,2 @@ export { default } from './ActionBar'; +export { default as downloadTarball } from './download-tarball'; diff --git a/src/components/ActionBar/styles.ts b/src/components/ActionBar/styles.ts deleted file mode 100644 index ebe2fd7..0000000 --- a/src/components/ActionBar/styles.ts +++ /dev/null @@ -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', -})); diff --git a/src/components/DetailSidebar/DetailSidebar.tsx b/src/components/DetailSidebar/DetailSidebar.tsx index fccb2e7..379a896 100644 --- a/src/components/DetailSidebar/DetailSidebar.tsx +++ b/src/components/DetailSidebar/DetailSidebar.tsx @@ -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'; diff --git a/src/components/Link/Link.tsx b/src/components/Link/Link.tsx index d0a5039..310cc4c 100644 --- a/src/components/Link/Link.tsx +++ b/src/components/Link/Link.tsx @@ -7,20 +7,26 @@ interface Props extends Pick { external?: boolean; className?: string; to: string; + children?: React.ReactNode; } +type LinkRef = HTMLAnchorElement; + /* eslint-disable verdaccio/jsx-spread */ -const Link: React.FC = ({ external, to, children, variant, className, ...props }) => { +const Link = React.forwardRef(function Link( + { external, to, children, variant, className, ...props }, + ref +) { const LinkTextContent = {children}; return external ? ( - + {LinkTextContent} ) : ( - + {LinkTextContent} ); -}; +}); export default Link; diff --git a/src/components/Package/Package.tsx b/src/components/Package/Package.tsx index b0b4fd3..0e84d6e 100644 --- a/src/components/Package/Package.tsx +++ b/src/components/Package/Package.tsx @@ -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 = ({ dist.tarball && isURL(dist.tarball) && ( // eslint-disable-next-line - downloadHandler(dist.tarball.replace(`https://registry.npmjs.org/`, window.location.href))} target={'_blank'}> + {/* eslint-disable-next-line react/jsx-max-depth */} diff --git a/types/packageMeta.ts b/types/packageMeta.ts index 3bf1853..0030e8f 100644 --- a/types/packageMeta.ts +++ b/types/packageMeta.ts @@ -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 | string; version: string; + homepage?: string; + bugs?: { + url: string; + }; repository?: { type?: string; url?: string;