feat: add browser features to browse by version (#125)

* feat: add browser features to browse by version

* chore: verify whether version exist

* chore: add link on versions

* chore: udpate imports

* chore: use mui links

* test: add unit test

* chore: Add todo list

* chore: remove imports
This commit is contained in:
Juan Picado @jotadeveloper 2019-09-01 04:09:23 -07:00 committed by GitHub
parent bbec54d602
commit 1904179af3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 6567 additions and 58 deletions

View File

@ -16,6 +16,15 @@ import { DetailContext } from '../../pages/Version';
import { TitleListItem, TitleListItemText } from './styles'; import { TitleListItem, TitleListItemText } from './styles';
const renderLatestDescription = (description, version, isLatest: boolean = true) => {
return (
<span>
<div>{description}</div>
{version ? <small>{`${isLatest ? 'Latest v' : 'v'}${version}`}</small> : null}
</span>
);
};
const renderCopyCLI = () => <Install />; const renderCopyCLI = () => <Install />;
const renderMaintainers = () => <Developers type="maintainers" />; const renderMaintainers = () => <Developers type="maintainers" />;
const renderContributors = () => <Developers type="contributors" />; const renderContributors = () => <Developers type="contributors" />;
@ -24,22 +33,25 @@ const renderAuthor = () => <Author />;
const renderEngine = () => <Engine />; const renderEngine = () => <Engine />;
const renderDist = () => <Dist />; const renderDist = () => <Dist />;
const renderActionBar = () => <ActionBar />; const renderActionBar = () => <ActionBar />;
const renderTitle = (packageName, packageMeta) => { const renderTitle = (packageName, packageVersion, packageMeta) => {
const version = packageVersion ? packageVersion : packageMeta.latest.version;
const isLatest = typeof packageVersion === 'undefined';
return ( return (
<List className="detail-info"> <List className="detail-info">
<TitleListItem alignItems="flex-start" button={true}> <TitleListItem alignItems="flex-start" button={true}>
<TitleListItemText primary={<b>{packageName}</b>} secondary={packageMeta.latest.description} /> <TitleListItemText primary={<b>{packageName}</b>} secondary={renderLatestDescription(packageMeta.latest.description, version, isLatest)} />
</TitleListItem> </TitleListItem>
</List> </List>
); );
}; };
function renderSideBar(packageName, packageMeta): ReactElement<HTMLElement> { function renderSideBar(packageName, packageVersion, packageMeta): ReactElement<HTMLElement> {
return ( return (
<div className={'sidebar-info'}> <div className={'sidebar-info'}>
<Card> <Card>
<CardContent> <CardContent>
{renderTitle(packageName, packageMeta)} {renderTitle(packageName, packageVersion, packageMeta)}
{renderActionBar()} {renderActionBar()}
{renderCopyCLI()} {renderCopyCLI()}
{renderRepository()} {renderRepository()}
@ -55,9 +67,9 @@ function renderSideBar(packageName, packageMeta): ReactElement<HTMLElement> {
} }
const DetailSidebar = () => { const DetailSidebar = () => {
const { packageName, packageMeta } = React.useContext(DetailContext); const { packageName, packageMeta, packageVersion } = React.useContext(DetailContext);
return renderSideBar(packageName, packageMeta); return renderSideBar(packageName, packageVersion, packageMeta);
}; };
export default DetailSidebar; export default DetailSidebar;

View File

@ -1,35 +1,20 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router';
import Versions from './Versions'; import Versions, { LABEL_CURRENT_TAGS, LABEL_VERSION_HISTORY } from './Versions';
import data from './__partials__/data.json';
import { render, cleanup } from '@testing-library/react';
const mockPackageMeta = jest.fn(() => ({ const mockPackageMeta = jest.fn(() => ({
latest: { packageName: 'foo',
versions: { packageMeta: data,
'1.0.0': {
version: '1.0.0',
},
'2.0.0': {
version: '2.0.0',
},
'3.0.0': {
version: '3.0.0',
},
},
time: {
'1.0.0': '2016-08-26T22:36:41.762Z',
'2.0.0': '2017-08-26T22:36:41.762Z',
'3.0.0': '2018-02-07T06:43:22.801Z',
},
'dist-tags': {
latest: '3.0.0',
},
},
})); }));
jest.mock('../../pages/Version', () => ({ jest.mock('../../pages/Version', () => ({
DetailContextConsumer: component => { DetailContextConsumer: component => {
return component.children({ packageMeta: mockPackageMeta() }); return component.children({ ...mockPackageMeta() });
}, },
})); }));
@ -38,8 +23,51 @@ describe('<Version /> component', () => {
jest.resetModules(); jest.resetModules();
}); });
afterEach(() => {
cleanup();
});
test('should render the component in default state', () => { test('should render the component in default state', () => {
const wrapper = mount(<Versions />); const wrapper = mount(
<MemoryRouter>
<Versions />
</MemoryRouter>
);
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
test('should render versions', () => {
const { getByText } = render(
<MemoryRouter>
<Versions />
</MemoryRouter>
);
expect(getByText(LABEL_VERSION_HISTORY)).toBeTruthy();
expect(getByText(LABEL_CURRENT_TAGS)).toBeTruthy();
// pick some versions
expect(getByText('2.3.0')).toBeTruthy();
expect(getByText('canary')).toBeTruthy();
});
test('should not render versions', () => {
const request = {
packageName: 'foo',
};
// @ts-ignore
mockPackageMeta.mockImplementation(() => request);
const { queryByText } = render(
<MemoryRouter>
<Versions />
</MemoryRouter>
);
expect(queryByText(LABEL_VERSION_HISTORY)).toBeFalsy();
expect(queryByText(LABEL_CURRENT_TAGS)).toBeFalsy();
});
test.todo('should click on version link');
}); });

View File

@ -1,57 +1,85 @@
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import { Link as RouterLink } from 'react-router-dom';
import Link from '@material-ui/core/Link';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import { DetailContextConsumer } from '../../pages/Version'; import { DetailContextConsumer } from '../../pages/Version';
import { formatDateDistance } from '../../utils/package'; import { formatDateDistance } from '../../utils/package';
import { DIST_TAGS } from '../../../lib/constants'; import { DIST_TAGS } from '../../../lib/constants';
import { Heading, Spacer, ListItemText } from './styles'; import { Heading, Spacer, ListItemText } from './styles';
const NOT_AVAILABLE = 'Not available'; export const NOT_AVAILABLE = 'Not available';
export const LABEL_CURRENT_TAGS = 'Current Tags';
export const LABEL_VERSION_HISTORY = 'Version History';
class Versions extends React.PureComponent { class Versions extends React.PureComponent {
public render(): ReactElement<HTMLDivElement> { public render(): ReactElement<HTMLDivElement> {
return ( return (
<DetailContextConsumer> <DetailContextConsumer>
{context => { {context => {
return context && context.packageMeta && this.renderContent(context.packageMeta); const { packageMeta, packageName } = context;
if (!packageMeta) {
return null;
}
return this.renderContent(packageMeta, packageName);
}} }}
</DetailContextConsumer> </DetailContextConsumer>
); );
} }
public renderPackageList = (packages: {}, isVersion: boolean = false, timeMap: Record<string, {}> = {}): ReactElement<HTMLDivElement> => { public renderPackageList = (packages: {}, timeMap: Record<string, {}>, packageName): ReactElement<HTMLDivElement> => {
return ( return (
<List> <List dense={true}>
{Object.keys(packages) {Object.keys(packages)
.reverse() .reverse()
.map(version => ( .map(version => (
<ListItem className="version-item" key={version}> <ListItem className="version-item" key={version}>
<ListItemText>{version}</ListItemText> <Link component={RouterLink} to={`/-/web/detail/${packageName}/v/${version}`}>
<ListItemText>{version}</ListItemText>
</Link>
<Spacer /> <Spacer />
{isVersion && <ListItemText>{timeMap[version] ? `${formatDateDistance(timeMap[version])} ago` : NOT_AVAILABLE}</ListItemText>} <ListItemText>{timeMap[version] ? `${formatDateDistance(timeMap[version])} ago` : NOT_AVAILABLE}</ListItemText>
{isVersion === false && <ListItemText>{packages[version]}</ListItemText>}
</ListItem> </ListItem>
))} ))}
</List> </List>
); );
}; };
public renderContent(packageMeta): ReactElement<HTMLDivElement> { public renderTagList = (packages: {}): ReactElement<HTMLDivElement> => {
return (
<List dense={true}>
{Object.keys(packages)
.reverse()
.map(tag => (
<ListItem className="version-item" key={tag}>
<ListItemText>{tag}</ListItemText>
<Spacer />
<ListItemText>{packages[tag]}</ListItemText>
</ListItem>
))}
</List>
);
};
public renderContent(packageMeta, packageName): ReactElement<HTMLDivElement> {
const { versions = {}, time: timeMap = {}, [DIST_TAGS]: distTags = {} } = packageMeta; const { versions = {}, time: timeMap = {}, [DIST_TAGS]: distTags = {} } = packageMeta;
return ( return (
<> <>
{distTags && ( {distTags && (
<> <>
<Heading variant="subtitle1">{'Current Tags'}</Heading> <Heading variant="subtitle1">{LABEL_CURRENT_TAGS}</Heading>
{this.renderPackageList(distTags, false, timeMap)} {this.renderTagList(distTags)}
</> </>
)} )}
{versions && ( {versions && (
<> <>
<Heading variant="subtitle1">{'Version History'}</Heading> <Heading variant="subtitle1">{LABEL_VERSION_HISTORY}</Heading>
{this.renderPackageList(versions, true, timeMap)} {this.renderPackageList(versions, timeMap, packageName)}
</> </>
)} )}
</> </>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -19,10 +19,27 @@ export function getRouterPackageName(params): string {
return packageName; return packageName;
} }
function fillTitle(text) {
return `Verdaccio - ${text}`;
}
function isVersionValid(packageMeta, packageVersion): boolean {
const hasVersion = typeof packageVersion !== 'undefined';
if (!hasVersion) {
// if is undefined, that means versions does not exist, we continue
return true;
}
const hasMatchVersion = Object.keys(packageMeta.versions).includes(packageVersion);
return hasMatchVersion;
}
const Version = ({ match: { params } }) => { const Version = ({ match: { params } }) => {
const pkgName = getRouterPackageName(params); const pkgName = getRouterPackageName(params);
const [readMe, setReadme] = useState(); const [readMe, setReadme] = useState();
const [packageName, setPackageName] = useState(pkgName); const [packageName, setPackageName] = useState(pkgName);
// eslint-disable-next-line no-unused-vars
const [packageVersion, setPackageVersion] = useState(params.version);
const [packageMeta, setPackageMeta] = useState(); const [packageMeta, setPackageMeta] = useState();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [notFound, setNotFound] = useState(false); const [notFound, setNotFound] = useState(false);
@ -30,27 +47,37 @@ const Version = ({ match: { params } }) => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const packageMeta = (await callDetailPage(packageName)) as Partial<StateInterface>; const packageMeta = (await callDetailPage(packageName, packageVersion)) as Partial<StateInterface>;
const readMe = (await callReadme(packageName)) as Partial<StateInterface>; const readMe = (await callReadme(packageName, packageVersion)) as Partial<StateInterface>;
setReadme(readMe); if (isVersionValid(packageMeta, packageVersion)) {
setPackageMeta(packageMeta); setReadme(readMe);
setIsLoading(false); setPackageMeta(packageMeta);
setIsLoading(false);
} else {
setIsLoading(false);
setNotFound(true);
}
} catch (error) { } catch (error) {
setNotFound(true); setNotFound(true);
setIsLoading(false); setIsLoading(false);
} }
})(); })();
}, [packageName]); }, [packageName, packageVersion]);
useEffect(() => { useEffect(() => {
document.title = `Verdaccio - ${packageName}`; if (!packageVersion) {
}, [packageName]); document.title = fillTitle(packageName);
} else {
document.title = fillTitle(`${packageName}@${packageVersion}`);
}
}, [packageName, packageVersion]);
useEffect(() => { useEffect(() => {
const pkgName = getRouterPackageName(params); const pkgName = getRouterPackageName(params);
setPackageName(pkgName); setPackageName(pkgName);
}, [params]); setPackageVersion(params.version);
}, [params, setPackageName, setPackageVersion]);
const isNotFound = notFound || typeof packageMeta === 'undefined' || typeof packageName === 'undefined' || typeof readMe === 'undefined'; const isNotFound = notFound || typeof packageMeta === 'undefined' || typeof packageName === 'undefined' || typeof readMe === 'undefined';
const renderContent = (): React.ReactElement<HTMLElement> => { const renderContent = (): React.ReactElement<HTMLElement> => {
@ -63,7 +90,9 @@ const Version = ({ match: { params } }) => {
} }
}; };
return <DetailContextProvider value={{ packageMeta, readMe, packageName, enableLoading: setIsLoading }}>{renderContent()}</DetailContextProvider>; return (
<DetailContextProvider value={{ packageMeta, packageVersion, readMe, packageName, enableLoading: setIsLoading }}>{renderContent()}</DetailContextProvider>
);
}; };
export default Version; export default Version;

View File

@ -2,6 +2,7 @@ import { PackageMetaInterface } from '../../../types/packageMeta';
export interface DetailContextProps { export interface DetailContextProps {
packageMeta: PackageMetaInterface; packageMeta: PackageMetaInterface;
packageVersion?: string;
readMe: string; readMe: string;
packageName: string; packageName: string;
enableLoading: () => void; enableLoading: () => void;
@ -11,6 +12,7 @@ export interface VersionPageConsumerProps {
packageMeta: PackageMetaInterface; packageMeta: PackageMetaInterface;
readMe: string; readMe: string;
packageName: string; packageName: string;
packageVersion?: string;
// FIXME: looking for the appropiated type here // FIXME: looking for the appropiated type here
enableLoading: any; enableLoading: any;
} }

View File

@ -31,6 +31,8 @@ class RouterApp extends Component<RouterAppProps> {
<Route exact={true} path={'/'} render={this.renderHomePage} /> <Route exact={true} path={'/'} render={this.renderHomePage} />
<Route exact={true} path={'/-/web/detail/@:scope/:package'} render={this.renderVersionPage} /> <Route exact={true} path={'/-/web/detail/@:scope/:package'} render={this.renderVersionPage} />
<Route exact={true} path={'/-/web/detail/:package'} render={this.renderVersionPage} /> <Route exact={true} path={'/-/web/detail/:package'} render={this.renderVersionPage} />
<Route exact={true} path={'/-/web/detail/:package/v/:version'} render={this.renderVersionPage} />
<Route exact={true} path={'/-/web/detail/@:scope/:package/v/:version'} render={this.renderVersionPage} />
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
</> </>

View File

@ -1,12 +1,12 @@
import API from './api'; import API from './api';
import { PackageMetaInterface } from 'types/packageMeta'; import { PackageMetaInterface } from 'types/packageMeta';
export async function callReadme(packageName): Promise<string | {}> { export async function callReadme(packageName, packageVersion?: string): Promise<string | {}> {
return await API.request<string | {}>(`package/readme/${packageName}`, 'GET'); return await API.request<string | {}>(`package/readme/${packageName}${packageVersion ? `?v=${packageVersion}` : ''}`, 'GET');
} }
export async function callDetailPage(packageName): Promise<PackageMetaInterface | {}> { export async function callDetailPage(packageName: string, packageVersion?: string): Promise<PackageMetaInterface | {}> {
const packageMeta = await API.request<PackageMetaInterface | {}>(`sidebar/${packageName}`, 'GET'); const packageMeta = await API.request<PackageMetaInterface | {}>(`sidebar/${packageName}${packageVersion ? `?v=${packageVersion}` : ''}`, 'GET');
return packageMeta; return packageMeta;
} }

View File

@ -18,7 +18,9 @@ new WebpackDevServer(compiler, {
contentBase: `${env.DIST_PATH}`, contentBase: `${env.DIST_PATH}`,
publicPath: config.output.publicPath, publicPath: config.output.publicPath,
hot: true, hot: true,
historyApiFallback: true, historyApiFallback: {
disableDotRule: true,
},
quiet: true, quiet: true,
noInfo: false, noInfo: false,
stats: { stats: {