forked from sombochea/verdaccio-ui
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:
parent
bbec54d602
commit
1904179af3
@ -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;
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
|
@ -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}>
|
||||||
|
<Link component={RouterLink} to={`/-/web/detail/${packageName}/v/${version}`}>
|
||||||
<ListItemText>{version}</ListItemText>
|
<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)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
6406
src/components/Versions/__partials__/data.json
Normal file
6406
src/components/Versions/__partials__/data.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -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>;
|
||||||
|
if (isVersionValid(packageMeta, packageVersion)) {
|
||||||
setReadme(readMe);
|
setReadme(readMe);
|
||||||
setPackageMeta(packageMeta);
|
setPackageMeta(packageMeta);
|
||||||
setIsLoading(false);
|
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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user