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:
commit
47cc15e72d
@ -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} />;
|
||||
};
|
||||
}
|
||||
|
3
src/webui/components/Icon/img/filebinary.svg
Normal file
3
src/webui/components/Icon/img/filebinary.svg
Normal file
@ -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 |
3
src/webui/components/Icon/img/law.svg
Normal file
3
src/webui/components/Icon/img/law.svg
Normal file
@ -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 |
6
src/webui/components/Icon/img/version.svg
Normal file
6
src/webui/components/Icon/img/version.svg
Normal file
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user