1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-08 23:25:51 +01:00

Merge pull request #1242 from verdaccio/4.x-package-list-improvements

4.x-package list improvements
This commit is contained in:
Juan Picado @jotadeveloper 2019-03-04 22:48:03 +01:00 committed by GitHub
commit 47cc15e72d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 460 additions and 382 deletions

@ -4,9 +4,10 @@
*/
import _ from 'lodash';
import { addScope, addGravatarSupport, deleteProperties, sortByName, parseReadme } from '../../../lib/utils';
import { addScope, addGravatarSupport, deleteProperties, sortByName, parseReadme, formatAuthor } from '../../../lib/utils';
import { allow } from '../../middleware';
import { DIST_TAGS, HEADER_TYPE, HEADERS, HTTP_STATUS } from '../../../lib/constants';
import { generateGravatarUrl } from '../../../utils/user';
import logger from '../../../lib/logger';
import type { Router } from 'express';
import type { IAuth, $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler, $SidebarPackage } from '../../../../types';
@ -41,12 +42,18 @@ function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth,
throw err;
}
async function processPermissionsPackages(packages) {
async function processPermissionsPackages(packages = []) {
const permissions = [];
for (const pkg of packages) {
const packgesCopy = packages.slice();
for (const pkg of packgesCopy) {
const pkgCopy = { ...pkg };
pkgCopy.author = formatAuthor(pkg.author);
try {
if (await checkAllow(pkg.name, req.remote_user)) {
permissions.push(pkg);
if (config.web) {
pkgCopy.author.avatar = generateGravatarUrl(pkgCopy.author.email, config.web.gravatar);
}
permissions.push(pkgCopy);
}
} catch (err) {
logger.logger.error({ name: pkg.name, error: err }, 'permission process for @{name} has failed: @{error}');
@ -96,6 +103,7 @@ function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth,
if (_.isNil(err)) {
let sideBarInfo: any = _.clone(info);
sideBarInfo.latest = info.versions[info[DIST_TAGS].latest];
sideBarInfo.latest.author = formatAuthor(sideBarInfo.latest.author);
sideBarInfo = deleteProperties(['readme', '_attachments', '_rev', 'name'], sideBarInfo);
if (config.web) {
sideBarInfo = addGravatarSupport(sideBarInfo, config.web.gravatar);

@ -11,6 +11,7 @@ export const TIME_EXPIRATION_24H: string = '24h';
export const TIME_EXPIRATION_7D: string = '7d';
export const DIST_TAGS = 'dist-tags';
export const DEFAULT_MIN_LIMIT_PASSWORD: number = 3;
export const DEFAULT_USER = 'Anonymous';
export const keyPem = 'verdaccio-key.pem';
export const certPem = 'verdaccio-cert.pem';

@ -12,7 +12,7 @@ import URL from 'url';
import createError from 'http-errors';
import marked from 'marked';
import { HTTP_STATUS, API_ERROR, DEFAULT_PORT, DEFAULT_DOMAIN, DEFAULT_PROTOCOL, CHARACTER_ENCODING, HEADERS, DIST_TAGS } from './constants';
import { HTTP_STATUS, API_ERROR, DEFAULT_PORT, DEFAULT_DOMAIN, DEFAULT_PROTOCOL, CHARACTER_ENCODING, HEADERS, DIST_TAGS, DEFAULT_USER } from './constants';
import { generateGravatarUrl, GENERIC_AVATAR } from '../utils/user';
import type { Package } from '@verdaccio/types';
@ -510,3 +510,40 @@ export function getVersionFromTarball(name: string) {
// $FlowFixMe
return /.+-(\d.+)\.tgz/.test(name) ? name.match(/.+-(\d.+)\.tgz/)[1] : undefined;
}
/**
* Formats author field for webui.
* @see https://docs.npmjs.com/files/package.json#author
* @param {string|object|undefined} author
*/
export function formatAuthor(author: any) {
let authorDetails = {
name: DEFAULT_USER,
email: '',
url: '',
};
if (!author) {
return authorDetails;
}
if (_.isString(author)) {
authorDetails = {
...authorDetails,
name: author ? author : authorDetails.name,
email: author.email ? author.email : authorDetails.email,
url: author.url ? author.url : authorDetails.url,
};
}
if (_.isObject(author)) {
authorDetails = {
...authorDetails,
name: author.name ? author.name : authorDetails.name,
email: author.email ? author.email : authorDetails.email,
url: author.url ? author.url : authorDetails.url,
};
}
return authorDetails;
}

@ -1,3 +1,7 @@
/**
* @prettier
*/
import React, { Component, Fragment } from 'react';
import isNil from 'lodash/isNil';
@ -16,7 +20,6 @@ import 'normalize.css';
import Footer from './components/Footer';
export const AppContext = React.createContext();
export const AppContextProvider = AppContext.Provider;
export const AppContextConsumer = AppContext.Consumer;
@ -25,12 +28,12 @@ export default class App extends Component {
error: {},
logoUrl: window.VERDACCIO_LOGO,
user: {},
scope: (window.VERDACCIO_SCOPE) ? `${window.VERDACCIO_SCOPE}:` : '',
scope: window.VERDACCIO_SCOPE ? `${window.VERDACCIO_SCOPE}:` : '',
showLoginModal: false,
isUserLoggedIn: false,
packages: [],
isLoading: true,
}
};
componentDidMount() {
this.isUserAlreadyLoggedIn();
@ -45,19 +48,36 @@ export default class App extends Component {
}
}
render() {
const { isLoading, isUserLoggedIn, packages, logoUrl, user, scope } = this.state;
return (
<Container isLoading={isLoading}>
{isLoading ? (
<Loading />
) : (
<Fragment>
<AppContextProvider value={{ isUserLoggedIn, packages, logoUrl, user, scope }}>{this.renderContent()}</AppContextProvider>
</Fragment>
)}
{this.renderLoginModal()}
</Container>
);
}
isUserAlreadyLoggedIn = () => {
// checks for token validity
const token = storage.getItem('token');
const username = storage.getItem('username');
if (isTokenExpire(token) || isNil(username)) {
this.handleLogout();
this.handleLogout();
} else {
this.setState({
user: { username, token },
isUserLoggedIn: true,
});
}
}
};
loadOnHandler = async () => {
try {
@ -74,34 +94,30 @@ export default class App extends Component {
});
this.setLoading(false);
}
}
};
setLoading = isLoading => (
setLoading = isLoading =>
this.setState({
isLoading,
})
)
});
/**
* Toggles the login modal
* Required by: <LoginModal /> <Header />
*/
handleToggleLoginModal = () => {
this.setState((prevState) => ({
this.setState(prevState => ({
showLoginModal: !prevState.showLoginModal,
error: {},
}));
}
};
/**
* handles login
* Required by: <Header />
*/
handleDoLogin = async (usernameValue, passwordValue) => {
const { username, token, error } = await makeLogin(
usernameValue,
passwordValue
);
const { username, token, error } = await makeLogin(usernameValue, passwordValue);
if (username && token) {
this.setLoggedUser(username, token);
@ -115,7 +131,7 @@ export default class App extends Component {
error,
});
}
}
};
setLoggedUser = (username, token) => {
this.setState({
@ -123,10 +139,11 @@ export default class App extends Component {
username,
token,
},
isUserLoggedIn: true, // close login modal after successful login
showLoginModal: false, // set isUserLoggedIn to true
isUserLoggedIn: true, // close login modal after successful login
showLoginModal: false, // set isUserLoggedIn to true
});
}
};
/**
* Logouts user
* Required by: <Header />
@ -138,25 +155,7 @@ export default class App extends Component {
user: {},
isUserLoggedIn: false,
});
}
render() {
const { isLoading, isUserLoggedIn, packages, logoUrl, user, scope } = this.state;
return (
<Container isLoading={isLoading}>
{isLoading ? (
<Loading />
) : (
<Fragment>
<AppContextProvider value={{isUserLoggedIn, packages, logoUrl, user, scope}}>
{this.renderContent()}
</AppContextProvider>
</Fragment>
)}
{this.renderLoginModal()}
</Container>
);
}
};
renderLoginModal = () => {
const { error, showLoginModal } = this.state;
@ -169,34 +168,24 @@ export default class App extends Component {
visibility={showLoginModal}
/>
);
}
};
renderContent = () => {
return (
<Fragment>
<Content>
<RouterApp
onLogout={this.handleLogout}
onToggleLoginModal={this.handleToggleLoginModal}>
<RouterApp onLogout={this.handleLogout} onToggleLoginModal={this.handleToggleLoginModal}>
{this.renderHeader()}
</RouterApp>
</Content>
<Footer />
</Fragment>
);
}
};
renderHeader = () => {
const { logoUrl, user, scope } = this.state;
const { logoUrl, user: { username } = {}, scope } = this.state;
return (
<Header
logo={logoUrl}
onLogout={this.handleLogout}
onToggleLoginModal={this.handleToggleLoginModal}
scope={scope}
username={user.username}
/>
);
}
return <Header logo={logoUrl} onLogout={this.handleLogout} onToggleLoginModal={this.handleToggleLoginModal} scope={scope} username={username} />;
};
}

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" id="filebinary">
<path d="M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM5 6.98L3.5 8.5 5 10l-.5 1L2 8.5 4.5 6l.5.98zM7.5 6L10 8.5 7.5 11l-.5-.98L8.5 8.5 7 7l.5-1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 265 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" id="law">
<path fill-rule="evenodd" d="M7 4c-.83 0-1.5-.67-1.5-1.5S6.17 1 7 1s1.5.67 1.5 1.5S7.83 4 7 4zm7 6c0 1.11-.89 2-2 2h-1c-1.11 0-2-.89-2-2l2-4h-1c-.55 0-1-.45-1-1H8v8c.42 0 1 .45 1 1h1c.42 0 1 .45 1 1H3c0-.55.58-1 1-1h1c0-.55.58-1 1-1h.03L6 5H5c0 .55-.45 1-1 1H3l2 4c0 1.11-.89 2-2 2H2c-1.11 0-2-.89-2-2l2-4H1V5h3c0-.55.45-1 1-1h4c.55 0 1 .45 1 1h3v1h-1l2 4zM2.5 7L1 10h3L2.5 7zM13 10l-1.5-3-1.5 3h3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 464 B

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 16" height="16" width="14" id="version">
<path fill-rule="evenodd" d="M13 3H7c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zm-1 8H8V5h4v6zM4 4h1v1H4v6h1v1H4c-.55 0-1-.45-1-1V5c0-.55.45-1 1-1zM1 5h1v1H1v4h1v1H1c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 344 B

@ -19,8 +19,11 @@ import austria from './img/austria.svg';
import spain from './img/spain.svg';
import earth from './img/earth.svg';
import verdaccio from './img/verdaccio.svg';
import filebinary from './img/filebinary.svg';
import law from './img/law.svg';
import license from './img/license.svg';
import time from './img/time.svg';
import version from './img/version.svg';
export const Icons: $Shape<IIconsMap> = {
// flags
@ -33,8 +36,12 @@ export const Icons: $Shape<IIconsMap> = {
austria,
earth,
verdaccio,
// other icons
filebinary,
law,
license,
time,
version,
};
const Icon = ({ className, name, size = 'sm', img = false, pointer = false, ...props }: IProps): Node => {

@ -21,7 +21,7 @@ const getSize = (size: string) => {
default:
return `
width: 14px;
height: 14px;
height: 16px;
`;
}
};

@ -17,6 +17,9 @@ export interface IIconsMap {
verdaccio: string;
license: string;
time: string;
law: string;
version: string;
filebinary: string;
[key: string]: string;
}

@ -2,104 +2,160 @@
* @prettier
* @flow
*/
import React from 'react';
import type { Element } from 'react';
import { spacing } from '../../utils/styles/mixings';
import BugReport from '@material-ui/icons/BugReport';
import Grid from '@material-ui/core/Grid/index';
import HomeIcon from '@material-ui/icons/Home';
import ListItem from '@material-ui/core/ListItem/index';
import Tooltip from '@material-ui/core/Tooltip/index';
import Tag from '../Tag';
import fileSizeSI from '../../utils/file-size';
import { formatDate, formatDateDistance } from '../../utils/package';
import { IProps } from './types';
import {
WrapperLink,
Header,
MainInfo,
Name,
Version,
Overview,
Published,
OverviewItem,
Description,
Icon,
Text,
Details,
Avatar,
Author,
Field,
Content,
Footer,
Avatar,
Description,
Details,
GridRightAligned,
Icon,
IconButton,
OverviewItem,
PackageList,
PackageListItem,
PackageListItemText,
PackageTitle,
Published,
TagContainer,
Text,
WrapperLink,
} from './styles';
const getInitialsName = (name: string) =>
name
.split(' ')
.reduce((accumulator, currentValue) => accumulator.charAt(0) + currentValue.charAt(0), '')
.toUpperCase();
const Package = ({
author: { name: authorName, avatar: authorAvatar },
bugs: { url } = {},
description,
dist: { unpackedSize } = {},
homepage,
keywords = [],
license,
name: packageName,
time,
version,
}: IProps): Element<WrapperLink> => {
//
const renderVersionInfo = () =>
version && (
<OverviewItem>
<Icon name={'version'} />
{`v${version}`}
</OverviewItem>
);
const Package = ({ name: label, version, time, author: { name, avatar }, description, license, keywords = [] }: IProps): Element<WrapperLink> => {
const renderMainInfo = () => (
<MainInfo>
<Name>{label}</Name>
<Version>{`v${version}`}</Version>
</MainInfo>
);
const renderAuthorInfo = () =>
authorName && (
<Author>
<Avatar alt={authorName} src={authorAvatar} />
<Details>
<Text text={authorName} />
</Details>
</Author>
);
const renderAuthorInfo = () => (
<Author>
<Avatar alt={name} src={avatar}>
{!avatar && getInitialsName(name)}
</Avatar>
<Details>
<Text text={name} weight={'bold'} />
</Details>
</Author>
);
const renderFileSize = () =>
unpackedSize && (
<OverviewItem>
<Icon name={'filebinary'} />
{fileSizeSI(unpackedSize)}
</OverviewItem>
);
const renderLicenseInfo = () =>
license && (
<OverviewItem>
<Icon modifiers={spacing('margin', '4px', '5px', '0px', '0px')} name={'license'} pointer={true} />
<Icon name={'law'} />
{license}
</OverviewItem>
);
const renderPublishedInfo = () => (
<OverviewItem>
<Icon name={'time'} pointer={true} />
<Published modifiers={spacing('margin', '0px', '5px', '0px', '0px')}>{`Published on ${formatDate(time)}`}</Published>
{`${formatDateDistance(time)} ago`}
</OverviewItem>
);
const renderDescription = () =>
description && (
<Field>
<Description>{description}</Description>
</Field>
const renderPublishedInfo = () =>
time && (
<OverviewItem>
<Icon name={'time'} />
<Published>{`Published on ${formatDate(time)}`}</Published>
{`${formatDateDistance(time)} ago`}
</OverviewItem>
);
const renderHomePageLink = () =>
homepage && (
<a href={homepage} target={'_blank'}>
<Tooltip aria-label={'Homepage'} title={'Visit homepage'}>
<IconButton aria-label={'Homepage'}>
{/* eslint-disable-next-line react/jsx-max-depth */}
<HomeIcon />
</IconButton>
</Tooltip>
</a>
);
const renderBugsLink = () =>
url && (
<a href={url} target={'_blank'}>
<Tooltip aria-label={'Bugs'} title={'Open an issue'}>
<IconButton aria-label={'Bugs'}>
{/* eslint-disable-next-line react/jsx-max-depth */}
<BugReport />
</IconButton>
</Tooltip>
</a>
);
const renderPrimaryComponent = () => {
return (
<Grid container={true} item={true} xs={12}>
<Grid item={true} xs={true}>
<WrapperLink to={`/-/web/detail/${packageName}`}>
{/* eslint-disable-next-line react/jsx-max-depth */}
<PackageTitle>{packageName}</PackageTitle>
</WrapperLink>
</Grid>
<GridRightAligned item={true} xs={true}>
{renderHomePageLink()}
{renderBugsLink()}
</GridRightAligned>
</Grid>
);
};
const renderSecondaryComponent = () => {
const tags = keywords.sort().map((keyword, index) => <Tag key={index}>{keyword}</Tag>);
return (
<>
<Description component={'span'}>{description}</Description>
{tags.length > 0 && <TagContainer>{tags}</TagContainer>}
</>
);
};
return (
<WrapperLink className={'package'} to={`/-/web/detail/${label}`}>
<Header>
{renderMainInfo()}
<Overview>
{renderLicenseInfo()}
{renderPublishedInfo()}
</Overview>
</Header>
<Content>
<Field>{renderAuthorInfo()}</Field>
{renderDescription()}
</Content>
{keywords.length > 0 && (
<Footer>
{keywords.sort().map((keyword, index) => (
<Tag key={index}>{keyword}</Tag>
))}
</Footer>
)}
</WrapperLink>
<PackageList className={'package'}>
<ListItem alignItems={'flex-start'}>
<PackageListItemText className={'package-link'} component={'div'} primary={renderPrimaryComponent()} secondary={renderSecondaryComponent()} />
</ListItem>
<PackageListItem alignItems={'flex-start'}>
{renderAuthorInfo()}
{renderVersionInfo()}
{renderPublishedInfo()}
{renderFileSize()}
{renderLicenseInfo()}
</PackageListItem>
</PackageList>
);
};
export default Package;

@ -3,126 +3,68 @@
* @flow
*/
import styled, { css } from 'react-emotion';
import styled from 'react-emotion';
import { Link } from 'react-router-dom';
import { default as Photo } from '@material-ui/core/Avatar';
import { default as Ico } from '../Icon';
import mq from '../../utils/styles/media';
import { ellipsis } from '../../utils/styles/mixings';
import colors from '../../utils/styles/colors';
import Grid from '@material-ui/core/Grid/index';
import List from '@material-ui/core/List/index';
import ListItem from '@material-ui/core/ListItem/index';
import ListItemText from '@material-ui/core/ListItemText/index';
import MuiIconButton from '@material-ui/core/IconButton/index';
import Photo from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography/index';
import { breakpoints } from '../../utils/styles/media';
import Ico from '../Icon';
import Label from '../Label';
// HEADER
export const Header = styled.div`
&& {
display: flex;
flex-direction: column;
padding: 0 0 5px 0;
}
`;
export const Name = styled.span`
&& {
color: ${colors.primary};
}
`;
export const MainInfo = styled.span`
&& {
font-size: 16px;
font-weight: 600;
line-height: 30px;
flex: 1;
color: #3a8bff;
padding: 0 10px 0 0;
overflow: hidden;
display: flex;
align-items: center;
:hover {
${Name} {
text-decoration: underline;
}
}
}
`;
import colors from '../../utils/styles/colors';
export const OverviewItem = styled.span`
&& {
display: flex;
align-items: center;
margin: 0 0 5px 0;
color: ${colors.greyLight};
}
`;
export const Overview = styled.span`
&& {
position: relative;
display: flex;
flex-direction: column;
}
`;
export const Version = styled.span`
&& {
margin: 0 0 0 16px;
color: ${colors.greyLight2};
font-size: 12px;
padding: 0 0 0 10px;
margin: 0 0 0 5px;
color: #9f9f9f;
position: relative;
${ellipsis('100%')};
:before {
content: '•';
position: absolute;
left: 0;
@media (max-width: ${breakpoints.medium}px) {
&:nth-child(3) {
display: none;
}
}
@media (max-width: ${breakpoints.small}px) {
&:nth-child(4) {
display: none;
}
}
}
`;
export const Icon = styled(Ico)`
&& {
margin: 1px 5px 0 0;
fill: ${colors.greyLight};
margin: 2px 10px 0px 0;
fill: ${colors.greyLight2};
}
`;
export const Published = styled.span`
&& {
display: none;
color: ${colors.greyLight};
${({ modifiers }) => modifiers};
}
`;
// Content
export const Field = styled.div`
&& {
padding: 0 0 5px 0;
}
`;
export const Content = styled.div`
&& {
${Field} {
:last-child {
padding: 0;
}
}
color: ${colors.greyLight2};
margin: 0px 5px 0px 0px;
}
`;
export const Text = styled(Label)`
&& {
color: #908ba1;
font-size: 12px;
font-weight: 500;
color: ${colors.greyLight2};
}
`;
export const Details = styled.span`
&& {
margin-left: 5px;
line-height: 14px;
line-height: 1.5;
display: flex;
flex-direction: column;
}
@ -137,63 +79,88 @@ export const Author = styled.div`
export const Avatar = styled(Photo)`
&& {
width: 30px;
height: 30px;
background: #4b5e40;
font-size: 15px;
width: 20px;
height: 20px;
}
`;
export const Description = styled.div`
&& {
margin: 5px 0;
}
`;
// Footer
export const Footer = styled.div`
&& {
display: none;
padding: 5px 0 0 0;
}
`;
// Container
export const WrapperLink = styled(Link)`
&& {
font-size: 12px;
background-color: white;
margin: 0 0 15px 0;
transition: box-shadow 0.15s;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15);
border-radius: 3px;
padding: 10px;
text-decoration: none;
display: block;
color: #2f273c;
${mq.medium(css`
${Header} {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
${OverviewItem} {
margin: 0 0 0 10px;
}
${Overview} {
flex-direction: row;
${OverviewItem} {
:first-child {
margin: 0;
}
}
}
${Footer} {
display: block;
}
${Published} {
display: inline-block;
}
`)};
}
`;
export const PackageTitle = styled.span`
&& {
font-weight: 600;
font-size: 20px;
display: block;
margin-bottom: 12px;
color: ${colors.eclipse};
cursor: pointer;
&:hover {
color: ${colors.black};
}
@media (max-width: ${breakpoints.small}px) {
font-size: 14px;
margin-bottom: 8px;
}
}
`;
export const GridRightAligned = styled(Grid)`
&& {
text-align: right;
}
`;
export const PackageList = styled(List)`
&& {
padding: 12px 0 12px 0;
&:hover {
background-color: ${colors.greyLight3};
}
}
`;
export const IconButton = styled(MuiIconButton)`
&& {
padding: 6px;
svg {
font-size: 16px;
}
}
`;
export const TagContainer = styled.span`
&& {
margin-top: 8px;
margin-bottom: 12px;
display: block;
@media (max-width: ${breakpoints.medium}px) {
display: none;
}
}
`;
export const PackageListItem = styled(ListItem)`
&& {
padding-top: 0;
}
`;
export const PackageListItemText = styled(ListItemText)`
&& {
padding-right: 0;
}
`;
export const Description = styled(Typography)`
color: ${colors.greyDark2};
font-size: 14px;
padding-right: 0;
`;

@ -11,6 +11,9 @@ export interface IProps {
description?: string;
keywords?: string[];
license?: string;
homepage: string;
bugs: IBugs;
dist: IDist;
}
export interface IAuthor {
@ -18,3 +21,10 @@ export interface IAuthor {
avatar: string;
email: string;
}
export interface IBugs {
url: string;
}
export interface IDist {
unpackedSize: number;
}

@ -1,9 +1,11 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import Divider from '@material-ui/core/Divider';
import Package from '../Package';
import Help from '../Help';
import { formatAuthor, formatLicense } from '../../utils/package';
import { formatLicense } from '../../utils/package';
import classes from './packageList.scss';
@ -12,28 +14,6 @@ export default class PackageList extends React.Component {
packages: PropTypes.array,
};
renderPackages = () => {
return (
<Fragment>
{this.renderList()}
</Fragment>
);
}
renderList = () => {
const { packages } = this.props;
return (
packages.map((pkg, i) => {
const { name, version, description, time, keywords } = pkg;
const author = formatAuthor(pkg.author);
const license = formatLicense(pkg.license);
return (
<Package key={i} {...{ name, version, author, description, license, time, keywords }} />
);
})
);
}
render() {
return (
<div className={"package-list-items"}>
@ -46,7 +26,32 @@ export default class PackageList extends React.Component {
hasPackages() {
const {packages} = this.props;
return packages.length > 0;
}
renderPackages = () => {
return (
<Fragment>
{this.renderList()}
</Fragment>
);
}
renderList = () => {
const { packages } = this.props;
return (
packages.map((pkg, i) => {
const { name, version, description, time, keywords, dist, homepage, bugs } = pkg;
const author = pkg.author;
// TODO: move format license to API side.
const license = formatLicense(pkg.license);
return (
<React.Fragment key={i}>
{i !== 0 && <Divider></Divider>}
<Package {...{ name, dist, version, author, description, license, time, keywords, homepage, bugs }} />
</React.Fragment>
);
})
);
}
}

@ -11,10 +11,10 @@ export const Wrapper = styled.span`
vertical-align: middle;
line-height: 22px;
border-radius: 2px;
color: #9f9f9f;
background-color: hsla(0, 0%, 51%, 0.1);
color: #485a3e;
background-color: #f3f4f2;
padding: 0.22rem 0.4rem;
margin: 5px 10px 0 0;
margin: 8px 8px 0 0;
${ellipsis('300px')};
}
`;

@ -1,24 +1,32 @@
/**
* @prettier
*/
import React from 'react';
export function asyncComponent(getComponent) {
return class AsyncComponent extends React.Component {
static Component = null;
state = {Component: AsyncComponent.Component};
state = { Component: AsyncComponent.Component };
componentDidMount() {
const {Component} = this.state;
const { Component } = this.state;
if (!Component) {
getComponent().then(({default: Component}) => {
AsyncComponent.Component = Component;
/* eslint react/no-did-mount-set-state:0 */
this.setState({Component});
});
getComponent()
.then(({ default: Component }) => {
AsyncComponent.Component = Component;
/* eslint react/no-did-mount-set-state:0 */
this.setState({ Component });
})
.catch(err => {
console.error(err);
});
}
}
render() {
const {Component} = this.state;
const { Component } = this.state;
if (Component) {
// eslint-disable-next-line verdaccio/jsx-spread
return <Component {...this.props} />;
}

@ -4,7 +4,6 @@ import format from 'date-fns/format';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
export const TIMEFORMAT = 'DD.MM.YYYY, HH:mm:ss';
export const DEFAULT_USER = 'Anonymous';
/**
* Formats license field for webui.
@ -39,36 +38,6 @@ export function formatRepository(repository) {
}
/**
* Formats author field for webui.
* @see https://docs.npmjs.com/files/package.json#author
*/
export function formatAuthor(author) {
let authorDetails = {
name: DEFAULT_USER,
email: '',
avatar: '',
};
if (isString(author)) {
authorDetails = {
...authorDetails,
name: author ? author : authorDetails.name,
};
}
if (isObject(author)) {
authorDetails = {
...authorDetails,
name: author.name ? author.name : authorDetails.name,
email: author.email ? author.email : authorDetails.email,
avatar: author.avatar ? author.avatar : authorDetails.avatar,
};
}
return authorDetails;
}
/**
* For <LastSync /> component
* @param {array} uplinks

@ -10,7 +10,10 @@ const colors = {
grey: '#808080',
greySuperLight: '#f5f5f5',
greyLight: '#d3d3d3',
greyLight2: '#908ba1',
greyLight3: '#f3f4f240',
greyDark: '#a9a9a9',
greyDark2: '#586069',
greyChateau: '#95989a',
greyGainsboro: '#e3e3e3',
greyAthens: '#d3dddd',

@ -1,6 +1,6 @@
import { css } from 'emotion';
const breakpoints = {
export const breakpoints = {
small: 576,
medium: 768,
large: 1024,

@ -22,7 +22,7 @@ describe('/ (Verdaccio Page)', () => {
};
const getPackages = async function() {
return await page.$$('.package-list-items .package');
return await page.$$('.package-list-items .package-link a');
};
const logIn = async function() {
@ -131,7 +131,6 @@ describe('/ (Verdaccio Page)', () => {
test('should navigate to the package detail', async () => {
const packagesList = await getPackages();
const firstPackage = packagesList[0];
await firstPackage.focus();
await firstPackage.click({ clickCount: 1, delay: 200 });
await page.waitFor(1000);
const readmeText = await page.evaluate(() => document.querySelector('.markdown-body').textContent);

@ -13,9 +13,10 @@ import {
normalizeDistTags,
getWebProtocol,
getVersionFromTarball,
sortByName
sortByName,
formatAuthor
} from '../../../src/lib/utils';
import { DIST_TAGS } from '../../../src/lib/constants';
import { DIST_TAGS, DEFAULT_USER } from '../../../src/lib/constants';
import Logger, { setup } from '../../../src/lib/logger';
import { readFile } from '../../functional/lib/test.utils';
@ -565,4 +566,26 @@ describe('Utilities', () => {
expect(addGravatarSupport(packageInfo)).toEqual(result);
});
});
describe('formatAuthor', () => {
test('should check author field different values', () => {
const author = 'verdaccioNpm';
expect(formatAuthor(author).name).toEqual(author);
});
test('should check author field for object value', () => {
const user = {
name: 'Verdaccion NPM',
email: 'verdaccio@verdaccio.org',
url: 'https://verdaccio.org'
};
expect(formatAuthor(user).url).toEqual(user.url);
expect(formatAuthor(user).email).toEqual(user.email);
expect(formatAuthor(user).name).toEqual(user.name);
});
test('should check author field for other value', () => {
expect(formatAuthor(null).name).toEqual(DEFAULT_USER);
expect(formatAuthor({}).name).toEqual(DEFAULT_USER);
expect(formatAuthor([]).name).toEqual(DEFAULT_USER);
});
});
});

@ -2,7 +2,6 @@ import React from 'react';
import { mount } from 'enzyme';
import storage from '../../../src/webui/utils/storage';
import App from '../../../src/webui/app';
import { API_ERROR } from '../../../src/lib/constants';
import { generateTokenWithTimeRange } from './components/__mocks__/token';
@ -84,7 +83,6 @@ describe('App', () => {
test('handleDoLogin - authentication failure', async () => {
const { handleDoLogin } = wrapper.instance();
await handleDoLogin('sam', '12345');
console.log(API_ERROR.BAD_USERNAME_PASSWORD);
const result = {
description: 'bad username/password, access denied',
title: 'Unable to login',

File diff suppressed because one or more lines are too long

@ -29,20 +29,20 @@ describe('<PackageList /> component', () => {
version: '1.0.0',
time: new Date(1532211072138).getTime(),
description: 'Private NPM repository',
author: { name: 'Sam' }
author: { name: 'Sam', avatar: 'test avatar' }
},
{
name: 'abc',
version: '1.0.1',
time: new Date(1532211072138).getTime(),
description: 'abc description',
author: { name: 'Rose' }
author: { name: 'Rose', avatar: 'test avatar' }
},
{
name: 'xyz',
version: '1.1.0',
description: 'xyz description',
author: { name: 'Martin' }
author: { name: 'Martin', avatar: 'test avatar' }
}
],
help: false

@ -8,7 +8,8 @@ export const packageInformation = [
homepage: 'https://jquery.com',
author: {
name: 'JS Foundation and other contributors',
url: 'https://github.com/jquery/jquery/blob/master/AUTHORS.txt'
url: 'https://github.com/jquery/jquery/blob/master/AUTHORS.txt',
avatar: '',
},
repository: {
type: 'git',
@ -108,6 +109,11 @@ export const packageInformation = [
license: 'MIT',
private: true,
main: 'lodash.js',
author: {
name: 'John david dalton',
url: 'test url',
avatar: 'test avatar',
},
engines: {
node: '>=4.0.0'
},

@ -4,8 +4,7 @@ import {
formatDate,
formatDateDistance,
getLastUpdatedPackageTime,
getRecentReleases,
formatAuthor, DEFAULT_USER
getRecentReleases
} from '../../../../src/webui/utils/package';
import { packageMeta } from '../components/store/packageMeta';
@ -44,28 +43,6 @@ describe('formatRepository', () => {
});
});
describe('formatAuthor', () => {
test('should check author field different values', () => {
const author = 'verdaccioNpm';
expect(formatAuthor(author).name).toEqual(author);
});
test('should check author field for object value', () => {
const user = {
name: 'Verdaccion NPM',
email: 'verdaccio@verdaccio.org',
url: 'https://verdaccio.org'
};
expect(formatAuthor(user).avatar).toEqual('');
expect(formatAuthor(user).email).toEqual(user.email);
expect(formatAuthor(user).name).toEqual(user.name);
});
test('should check author field for other value', () => {
expect(formatAuthor(null).name).toEqual(DEFAULT_USER);
expect(formatAuthor({}).name).toEqual(DEFAULT_USER);
expect(formatAuthor([]).name).toEqual(DEFAULT_USER);
});
});
describe('formatDate', () => {
test('should format the date', () => {
const date = 1532211072138;
@ -75,13 +52,13 @@ describe('formatDate', () => {
describe('formatDateDistance', () => {
test('should calculate the distance', () => {
const dateOneMonthAgo = () => {
const dateTwoMonthsAgo = () => {
const date = new Date();
date.setMonth(date.getMonth() - 1);
date.setMonth(date.getMonth() - 2);
return date;
};
const date = dateOneMonthAgo();
expect(formatDateDistance(date)).toEqual('about 1 month');
const date = dateTwoMonthsAgo();
expect(formatDateDistance(date)).toEqual('about 2 months');
});
});