mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-01-06 22:40:26 -05:00
feat: add published date and license on ui
This commit is contained in:
parent
c241007ee9
commit
ecbe616e46
10 changed files with 187 additions and 78 deletions
|
@ -382,7 +382,11 @@ class Storage implements IStorageHandler {
|
|||
const latest = info[DIST_TAGS].latest;
|
||||
|
||||
if (latest && info.versions[latest]) {
|
||||
packages.push(info.versions[latest]);
|
||||
const version = info.versions[latest];
|
||||
const time = info.time[latest];
|
||||
version.time = time;
|
||||
|
||||
packages.push(version);
|
||||
} else {
|
||||
self.logger.warn( {package: locals[itemPkg]}, 'package @{package} does not have a "latest" tag?' );
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import {Tag} from 'element-react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import isNil from 'lodash/isNil';
|
||||
import {formatDateDistance} from '../../utils/DateUtils';
|
||||
|
||||
import classes from './package.scss';
|
||||
|
||||
|
@ -12,24 +13,74 @@ export default class Package extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
package: pkg
|
||||
} = this.props;
|
||||
const {package: pkg} = this.props;
|
||||
|
||||
return (
|
||||
<Link to={`detail/${pkg.name}`} className={classes.package}>
|
||||
<h1>{pkg.name}<Tag type="gray">v{pkg.version}</Tag></h1>
|
||||
{this.renderAuthor(pkg)}
|
||||
<p>{pkg.description}</p>
|
||||
<section className={classes.package}>
|
||||
<Link to={`detail/${pkg.name}`}>
|
||||
<div className={classes.header}>
|
||||
{this.renderTitle(pkg)}
|
||||
{this.renderAuthor(pkg)}
|
||||
</div>
|
||||
<div className={classes.footer}>
|
||||
{this.renderDescription(pkg)}
|
||||
</div>
|
||||
<div className={classes.details}>
|
||||
{this.renderPublished(pkg)}
|
||||
{this.renderLicense(pkg)}
|
||||
</div>
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderPublished(pkg) {
|
||||
if (pkg.time) {
|
||||
return (<div className={classes.homepage}>
|
||||
{`Published ${formatDateDistance(pkg.time)} ago`}
|
||||
</div>);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderLicense(pkg) {
|
||||
if (pkg.license) {
|
||||
return (<div className={classes.license}>
|
||||
{pkg.license}
|
||||
</div>);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderDescription(pkg) {
|
||||
return (
|
||||
<p className={classes.description}>
|
||||
{pkg.description}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle(pkg) {
|
||||
return (
|
||||
<div className={classes.title}>
|
||||
<h1>
|
||||
{pkg.name} {this.renderTag(pkg)}
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTag(pkg) {
|
||||
return <Tag type="gray">v{pkg.version}</Tag>;
|
||||
}
|
||||
|
||||
renderAuthor(pkg) {
|
||||
if (isNil(pkg.author) || isNil(pkg.author.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <span role="author" className={classes.author}>By: {pkg.author.name}</span>;
|
||||
return <div role="author" className={classes.author}>{`By: ${pkg.author.name}`}</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +1,90 @@
|
|||
@import '../../styles/variable';
|
||||
|
||||
.package {
|
||||
display: block;
|
||||
position: relative;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
.footer {
|
||||
display: flex;
|
||||
p.description {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
font-size: 14px;
|
||||
color: darkgray;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.el-tag {
|
||||
margin-left: 5px;
|
||||
.details {
|
||||
display: flex;
|
||||
font-size: 80%;
|
||||
color: $description_color;
|
||||
padding-top: 5px;
|
||||
.license {
|
||||
width: 20%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.homepage {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
display: block;
|
||||
position: relative;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
|
||||
:global {
|
||||
.el-tag {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
color: $description_color;
|
||||
font-size: inherit;
|
||||
width: 30%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 0;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Author
|
||||
.author {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
color: lightgrey;
|
||||
font-size: inherit;
|
||||
word-wrap: break-word;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
font-size: 14px;
|
||||
color: darkgray;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 0;
|
||||
left: 0;
|
||||
content: 'Click to view detail';
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import format from 'date-fns/format';
|
||||
import Module from '../../Module';
|
||||
import classes from './style.scss';
|
||||
import {formatDate} from '../../../../utils/DateUtils';
|
||||
|
||||
const TIMEFORMAT = 'YYYY/MM/DD, HH:mm:ss';
|
||||
import classes from './style.scss';
|
||||
|
||||
export default class LastSync extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -21,14 +20,14 @@ export default class LastSync extends React.Component {
|
|||
}
|
||||
});
|
||||
|
||||
const time = format(new Date(lastUpdate), TIMEFORMAT);
|
||||
const time = formatDate(lastUpdate);
|
||||
|
||||
return lastUpdate ? time : '';
|
||||
}
|
||||
|
||||
get recentReleases() {
|
||||
let recentReleases = Object.keys(this.props.packageMeta.time).map((version) => {
|
||||
const time = format(new Date(this.props.packageMeta.time[version]), TIMEFORMAT);
|
||||
const time = formatDate(this.props.packageMeta.time[version]);
|
||||
return {version, time};
|
||||
});
|
||||
|
||||
|
|
|
@ -40,7 +40,9 @@ export default class Maintainers extends React.Component {
|
|||
}
|
||||
|
||||
get uniqueContributors() {
|
||||
if (!this.contributors) return [];
|
||||
if (!this.contributors) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return uniqBy(this.contributors, (contributor) => contributor.name).slice(0, 5);
|
||||
}
|
||||
|
@ -56,7 +58,11 @@ export default class Maintainers extends React.Component {
|
|||
|
||||
return (this.showAllContributors ? this.contributors : this.uniqueContributors)
|
||||
.map((contributor, index) => {
|
||||
return <MaintainerInfo key={index} title="Contributors" name={contributor.name} avatar={contributor.avatar}/>;
|
||||
return <MaintainerInfo
|
||||
key={index}
|
||||
title="Contributors"
|
||||
name={contributor.name}
|
||||
avatar={contributor.avatar}/>;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
/* Variables */
|
||||
|
||||
$break-small: 800px;
|
||||
$break-large: 1240px;
|
||||
$description_color: lightgrey;
|
||||
|
||||
@mixin container-size {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 100%;
|
||||
min-width: 400px;
|
||||
max-width: 1140px;
|
||||
max-width: $break-small;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
max-width: $break-large;
|
||||
}
|
||||
}
|
||||
|
||||
$space-lg: 30px;
|
||||
|
|
12
src/webui/src/utils/DateUtils.js
Normal file
12
src/webui/src/utils/DateUtils.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export const TIMEFORMAT = 'YYYY/MM/DD, HH:mm:ss';
|
||||
import format from 'date-fns/format';
|
||||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||
|
||||
|
||||
export function formatDate(lastUpdate) {
|
||||
return format(new Date(lastUpdate), TIMEFORMAT);
|
||||
}
|
||||
|
||||
export function formatDateDistance(lastUpdate) {
|
||||
return distanceInWordsToNow(new Date(lastUpdate));
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Package /> component should load the component 1`] = `"<a class=\\"package\\" href=\\"detail/verdaccio\\"><h1>verdaccio<span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1><span role=\\"author\\" class=\\"author\\">By: Sam</span><p>Private NPM repository</p></a>"`;
|
||||
exports[`<Package /> component should load the component 1`] = `"<section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Sam</div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"details\\"><div class=\\"homepage\\">Published about 1 month ago</div><div class=\\"license\\">MIT</div></div></a></section>"`;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PackageList /> component should load the component with packages 1`] = `"<div class=\\"package-list-items\\"><div class=\\"pkgContainer\\"><h1 class=\\"listTitle\\">Available Packages</h1><li><a class=\\"package\\" href=\\"detail/verdaccio\\"><h1>verdaccio<span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1><span role=\\"author\\" class=\\"author\\">By: Sam</span><p>Private NPM repository</p></a></li><li><a class=\\"package\\" href=\\"detail/abc\\"><h1>abc<span class=\\"el-tag el-tag--gray\\">v1.0.1</span></h1><span role=\\"author\\" class=\\"author\\">By: Rose</span><p>abc description</p></a></li><li><a class=\\"package\\" href=\\"detail/xyz\\"><h1>xyz<span class=\\"el-tag el-tag--gray\\">v1.1.0</span></h1><span role=\\"author\\" class=\\"author\\">By: Martin</span><p>xyz description</p></a></li></div></div>"`;
|
||||
exports[`<PackageList /> component should load the component with packages 1`] = `"<div class=\\"package-list-items\\"><div class=\\"pkgContainer\\"><h1 class=\\"listTitle\\">Available Packages</h1><li><section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Sam</div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"details\\"></div></a></section></li><li><section class=\\"package\\"><a href=\\"detail/abc\\"><div class=\\"header\\"><div class=\\"title\\"><h1>abc <span class=\\"el-tag el-tag--gray\\">v1.0.1</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Rose</div></div><div class=\\"footer\\"><p class=\\"description\\">abc description</p></div><div class=\\"details\\"></div></a></section></li><li><section class=\\"package\\"><a href=\\"detail/xyz\\"><div class=\\"header\\"><div class=\\"title\\"><h1>xyz <span class=\\"el-tag el-tag--gray\\">v1.1.0</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Martin</div></div><div class=\\"footer\\"><p class=\\"description\\">xyz description</p></div><div class=\\"details\\"></div></a></section></li></div></div>"`;
|
||||
|
|
|
@ -12,6 +12,8 @@ describe('<Package /> component', () => {
|
|||
const props = {
|
||||
name: 'verdaccio',
|
||||
version: '1.0.0',
|
||||
time: '2018-05-03T23:36:55.046Z',
|
||||
license: 'MIT',
|
||||
description: 'Private NPM repository',
|
||||
author: { name: 'Sam' }
|
||||
};
|
||||
|
@ -28,15 +30,15 @@ describe('<Package /> component', () => {
|
|||
|
||||
// integration expectations
|
||||
expect(wrapper.find('a').prop('href')).toEqual('detail/verdaccio');
|
||||
expect(wrapper.find('h1').text()).toEqual('verdacciov1.0.0');
|
||||
expect(wrapper.find('h1').text()).toEqual('verdaccio v1.0.0');
|
||||
expect(wrapper.find('.el-tag--gray').text()).toEqual('v1.0.0');
|
||||
expect(
|
||||
wrapper
|
||||
.find('span')
|
||||
.filterWhere(n => n.prop('role') === 'author')
|
||||
wrapper.find('div').filterWhere(n => n.prop('role') === 'author')
|
||||
.text()
|
||||
).toEqual('By: Sam');
|
||||
expect(wrapper.find('p').text()).toEqual('Private NPM repository');
|
||||
expect(wrapper.find('.homepage').text()).toMatch(/Published about/);
|
||||
expect(wrapper.find('.license').text()).toMatch(/MIT/);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue