0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2025-03-18 02:22:46 -05:00

feat: ui improvements (#4071)

* feat: add download tarball indicator

* Update ActionBar.test.tsx

* fix: dark mode and readme css

* Create long-jars-collect.md

* Update long-jars-collect.md

* feat: improve install commands

* snapshots

* feat: hide deprecated versions option

* add testss

* feat: display deprecated versions

* add i18n

* fix e2e

* Update Readme.spec.tsx.snap
This commit is contained in:
Juan Picado 2023-10-14 19:19:52 +02:00 committed by GitHub
parent 654caefff9
commit 580319a53a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 2811 additions and 1157 deletions

View file

@ -0,0 +1,15 @@
---
'@verdaccio/ui-theme': minor
'@verdaccio/ui-components': minor
---
feat: ui improvements
Some UI improvements
- download progress indicator: https://github.com/verdaccio/verdaccio/discussions/4068
- fix dark mode and readme css support https://github.com/verdaccio/verdaccio/discussions/3942 https://github.com/verdaccio/verdaccio/discussions/3467
- fix global for yarn packages and add version to the packages on copy
- feat: hide deprecated versions option
- fix: improve deprecated package style
- feat: display deprecated versions

View file

@ -98,6 +98,7 @@ export type CommonWebConf = {
showFooter?: boolean;
showThemeSwitch?: boolean;
showDownloadTarball?: boolean;
hideDeprecatedVersions?: boolean;
primaryColor: string;
showRaw?: boolean;
};

View file

@ -42,6 +42,7 @@ export default function renderHTML(config: ConfigYaml, manifest, manifestFiles,
const base = getPublicUrl(config?.url_prefix, req);
const basename = new URL(base).pathname;
const language = config?.i18n?.web ?? DEFAULT_LANGUAGE;
const hideDeprecatedVersions = config?.web?.hideDeprecatedVersions ?? false;
// @ts-ignore
const needHtmlCache = [undefined, null].includes(config?.web?.html_cache)
? true
@ -99,6 +100,7 @@ export default function renderHTML(config: ConfigYaml, manifest, manifestFiles,
title,
scope,
language,
hideDeprecatedVersions,
};
let webPage;

View file

@ -33,7 +33,7 @@
"babel-loader": "8.3.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"country-flag-icons": "1.5.5",
"css-loader": "6.7.3",
"css-loader": "6.8.1",
"dayjs": "1.11.7",
"dompurify": "2.4.5",
"friendly-errors-webpack-plugin": "1.7.0",

View file

@ -53,7 +53,9 @@
"versions": {
"current-tags": "Current Tags",
"version-history": "Version History",
"not-available": "Not available"
"not-available": "Not available",
"deprecated": "Deprecated",
"hide-deprecated": "All deprecated versions are hidden by global configuration"
},
"package": {
"published-on": "Published on {{time}} •",
@ -101,13 +103,8 @@
},
"installation": {
"title": "Installation",
"install-using-yarn": "Install using yarn",
"install-using-yarn-command": "yarn add {{packageName}}",
"install-using-npm": "Install using npm",
"install-using-npm-command": "npm install {{packageName}}",
"install-using-pnpm": "Install using pnpm",
"install-using-pnpm-command": "pnpm install {{packageName}}",
"global": "View as global"
"global": "global package",
"yarnModern": "yarn modern syntax"
},
"repository": {
"title": "Repository"

View file

@ -4,6 +4,7 @@ web:
primary_color: #CCC
# showRaw: true
# darkMode: true
hideDeprecatedVersions: false
pkgManagers:
- npm
- yarn

View file

@ -59,7 +59,16 @@ module.exports = {
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: true,
},
},
],
},
{
test: /\.md$/,

View file

@ -28,7 +28,6 @@ module.exports = Object.assign({}, config, {
'\\.(svg)$': '<rootDir>/jest/unit/empty.ts',
'\\.(jpg)$': '<rootDir>/jest/unit/empty.ts',
'\\.(md)$': '<rootDir>/jest/unit/empty-string.ts',
'github-markdown-css': '<rootDir>/jest/identity.js',
'react-markdown': '<rootDir>/src/__mocks__/react-markdown.tsx',
'remark-*': '<rootDir>/src/__mocks__/remark-plugin.ts',
},

View file

@ -14,6 +14,7 @@
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --no-copy-ignored --extensions \".ts,.tsx\" --source-maps --ignore \"src/**/*.stories.tsx\" --ignore \"src/**/*.test.tsx\" --ignore \"src/**/*.test.ts\"",
"watch": "babel src/ --out-dir build/ --watch --copy-files --no-copy-ignored --extensions \".ts,.tsx\" --source-maps --ignore \"src/**/*.stories.tsx\" --ignore \"src/**/*.test.tsx\" --ignore \"src/**/*.test.ts\"",
"build": "pnpm run build:js && pnpm run build:types",
"start": "start-storybook -p 6006 -s ./public",
"build-storybook": "build-storybook"
@ -33,9 +34,9 @@
"country-flag-icons": "1.5.5",
"dayjs": "1.11.7",
"dompurify": "2.4.5",
"github-markdown-css": "5.2.0",
"highlight.js": "11.7.0",
"history": "4.10.1",
"classnames": "2.3.2",
"i18next": "20.6.1",
"js-base64": "3.7.5",
"localstorage-memory": "1.0.3",

View file

@ -20,7 +20,7 @@ const packageMeta = {
dist: {
fileCount: 0,
unpackedSize: 0,
tarball: 'https://registry.verdaccio.org/storybook/-/storybook-0.2.0.tgz',
tarball: 'https://registry.npmjs.org/verdaccio/-/verdaccio-5.26.0.tgz',
},
homepage: 'https://verdaccio.org',
bugs: {

View file

@ -1,7 +1,13 @@
import React from 'react';
import { store } from '../../store/store';
import { cleanup, fireEvent, renderWithStore, screen } from '../../test/test-react-testing-library';
import {
cleanup,
fireEvent,
renderWithStore,
screen,
waitFor,
} from '../../test/test-react-testing-library';
import ActionBar from './ActionBar';
const defaultPackageMeta = {
@ -27,8 +33,10 @@ describe('<ActionBar /> component', () => {
});
test('should render the component in default state', () => {
const { container } = renderWithStore(<ActionBar packageMeta={defaultPackageMeta} />, store);
expect(container.firstChild).toMatchSnapshot();
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} />, store);
expect(screen.getByTestId('download-tarball-btn')).toBeInTheDocument();
expect(screen.getByTestId('BugReportIcon')).toBeInTheDocument();
expect(screen.getByTestId('HomeIcon')).toBeInTheDocument();
});
test('when there is no action bar data', () => {
@ -45,8 +53,10 @@ describe('<ActionBar /> component', () => {
},
};
const { container } = renderWithStore(<ActionBar packageMeta={packageMeta} />, store);
expect(container.firstChild).toMatchSnapshot();
renderWithStore(<ActionBar packageMeta={packageMeta} />, store);
expect(screen.queryByTestId('download-tarball-btn')).not.toBeInTheDocument();
expect(screen.queryByTestId('BugReportIcon')).not.toBeInTheDocument();
expect(screen.queryByTestId('HomeIcon')).not.toBeInTheDocument();
});
test('when there is a button to download a tarball', () => {
@ -59,11 +69,10 @@ describe('<ActionBar /> component', () => {
expect(screen.getByLabelText('action-bar-action.raw')).toBeTruthy();
});
test('when click button to raw manifest open a dialog with viewver', () => {
test('when click button to raw manifest open a dialog with viewer', async () => {
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
fireEvent.click(screen.getByLabelText('action-bar-action.raw'));
// TODO: fix this part
// expect(screen.getByTestId('raw-viewver-dialog')).toBeInTheDocument();
await waitFor(() => expect(screen.getByTestId('rawViewer--dialog')).toBeInTheDocument());
});
test('should not display download tarball button', () => {
@ -78,8 +87,6 @@ describe('<ActionBar /> component', () => {
test('when there is a button to open an issue', () => {
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} />, store);
// TODO: should be visible by text
// expect(screen.getByLabelText('action-bar-action.open-an-issue')).toBeTruthy();
expect(screen.getByTestId('BugReportIcon')).toBeInTheDocument();
});
});

View file

@ -1,4 +1,6 @@
/* eslint-disable verdaccio/jsx-spread */
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import React, { useState } from 'react';
import { url } from '../../utils';
@ -40,18 +42,20 @@ const ActionBar: React.FC<Props> = ({ showRaw, showDownloadTarball = true, packa
return (
<Box alignItems="center" display="flex" marginBottom="14px">
{actions.map((action) => (
<ActionBarAction key={action.type} {...action} />
))}
{isRawViewerOpen && (
<RawViewer
isOpen={isRawViewerOpen}
onClose={() => {
setIsRawViewerOpen(false);
}}
packageMeta={packageMeta}
/>
)}
<Stack direction="row" spacing={1}>
{actions.map((action) => (
<ActionBarAction key={action.type} {...action} />
))}
{isRawViewerOpen && (
<RawViewer
isOpen={isRawViewerOpen}
onClose={() => {
setIsRawViewerOpen(false);
}}
packageMeta={packageMeta}
/>
)}
</Stack>
</Box>
);
};

View file

@ -3,25 +3,21 @@ import BugReportIcon from '@mui/icons-material/BugReport';
import DownloadIcon from '@mui/icons-material/CloudDownload';
import HomeIcon from '@mui/icons-material/Home';
import RawOnIcon from '@mui/icons-material/RawOn';
import CircularProgress from '@mui/material/CircularProgress';
import FabMUI from '@mui/material/Fab';
import Tooltip from '@mui/material/Tooltip';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { Theme } from '../../Theme';
import { Dispatch } from '../../store/store';
import { Dispatch, RootState } from '../../store/store';
import { Link } from '../Link';
export const Fab = styled(FabMUI)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor:
theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue,
color: theme?.palette.white,
marginRight: 10,
':hover': {
color: theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue,
background: theme?.palette.white,
},
}));
type ActionType = 'VISIT_HOMEPAGE' | 'OPEN_AN_ISSUE' | 'DOWNLOAD_TARBALL' | 'RAW_DATA';
@ -36,6 +32,7 @@ export interface ActionBarActionProps {
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link, action }) => {
const { t } = useTranslation();
const dispatch = useDispatch<Dispatch>();
const isLoading = useSelector((state: RootState) => state?.loading?.models.download);
const handleDownload = useCallback(async () => {
dispatch.download.getTarball({ link });
@ -65,9 +62,17 @@ const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link, action })
case 'DOWNLOAD_TARBALL':
return (
<Tooltip title={t('action-bar-action.download-tarball') as string}>
<Fab data-testid="download-tarball-btn" onClick={handleDownload} size="small">
<DownloadIcon />
</Fab>
{isLoading ? (
<CircularProgress sx={{ marginX: 0 }}>
<Fab data-testid="download-tarball-btn" onClick={handleDownload} size="small">
<DownloadIcon />
</Fab>
</CircularProgress>
) : (
<Fab data-testid="download-tarball-btn" onClick={handleDownload} size="small">
<DownloadIcon />
</Fab>
)}
</Tooltip>
);
case 'RAW_DATA':

View file

@ -1,263 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ActionBar /> component should render the component in default state 1`] = `
.emotion-0 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
margin-bottom: 14px;
}
.emotion-1 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.75;
text-transform: uppercase;
}
.emotion-3 {
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
position: relative;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
background-color: transparent;
outline: 0;
border: 0;
margin: 0;
border-radius: 0;
padding: 0;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: middle;
-moz-appearance: none;
-webkit-appearance: none;
-webkit-text-decoration: none;
text-decoration: none;
color: inherit;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.75;
text-transform: uppercase;
min-height: 36px;
-webkit-transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
border-radius: 50%;
padding: 0;
min-width: 0;
width: 40px;
height: 40px;
z-index: 1050;
box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12);
color: rgba(0, 0, 0, 0.87);
background-color: #e0e0e0;
background-color: #4b5e40;
color: #fff;
margin-right: 10px;
}
.emotion-3::-moz-focus-inner {
border-style: none;
}
.emotion-3.Mui-disabled {
pointer-events: none;
cursor: default;
}
@media print {
.emotion-3 {
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
}
.emotion-3:active {
box-shadow: 0px 7px 8px -4px rgba(0,0,0,0.2),0px 12px 17px 2px rgba(0,0,0,0.14),0px 5px 22px 4px rgba(0,0,0,0.12);
}
.emotion-3:hover {
background-color: #f5f5f5;
-webkit-text-decoration: none;
text-decoration: none;
}
@media (hover: none) {
.emotion-3:hover {
background-color: #e0e0e0;
}
}
.emotion-3.Mui-focusVisible {
box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12);
}
.emotion-3.Mui-disabled {
color: rgba(0, 0, 0, 0.26);
box-shadow: none;
background-color: rgba(0, 0, 0, 0.12);
}
.emotion-3:hover {
color: #4b5e40;
background: #fff;
}
.emotion-4 {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 1em;
height: 1em;
display: inline-block;
fill: currentColor;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
-webkit-transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
font-size: 1.5rem;
}
.emotion-5 {
overflow: hidden;
pointer-events: none;
position: absolute;
z-index: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: inherit;
}
<div
class="MuiBox-root emotion-0"
>
<a
class=""
href="https://verdaccio.org"
rel="noopener noreferrer"
target="_blank"
>
<span
class="MuiTypography-root MuiTypography-button emotion-1"
>
<button
class="MuiButtonBase-root MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default emotion-2 emotion-3"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium emotion-4"
data-testid="HomeIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"
/>
</svg>
<span
class="MuiTouchRipple-root emotion-5"
/>
</button>
</span>
</a>
<a
class=""
href="https://github.com/verdaccio/monorepo/issues"
rel="noopener noreferrer"
target="_blank"
>
<span
class="MuiTypography-root MuiTypography-button emotion-1"
>
<button
class="MuiButtonBase-root MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default emotion-2 emotion-3"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium emotion-4"
data-testid="BugReportIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"
/>
</svg>
<span
class="MuiTouchRipple-root emotion-5"
/>
</button>
</span>
</a>
<button
aria-label="action-bar-action.download-tarball"
class="MuiButtonBase-root MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default MuiFab-root MuiFab-circular MuiFab-sizeSmall MuiFab-default emotion-2 emotion-3"
data-mui-internal-clone-element="true"
data-testid="download-tarball-btn"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium emotion-4"
data-testid="CloudDownloadIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"
/>
</svg>
<span
class="MuiTouchRipple-root emotion-5"
/>
</button>
</div>
`;
exports[`<ActionBar /> component when there is no action bar data 1`] = `
.emotion-0 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
margin-bottom: 14px;
}
<div
class="MuiBox-root emotion-0"
/>
`;

View file

@ -1,11 +1,13 @@
import { Typography } from '@mui/material';
import Avatar from '@mui/material/Avatar';
import List from '@mui/material/List';
import { useTheme } from '@mui/styles';
import i18next from 'i18next';
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { url } from '../../utils';
import { AuthorListItem, AuthorListItemText, StyledText } from './styles';
import { AuthorListItem, StyledText } from './styles';
export function getAuthorName(authorName?: string): string {
if (!authorName) {
@ -26,6 +28,8 @@ export function getAuthorName(authorName?: string): string {
const Author: FC<{ packageMeta }> = ({ packageMeta }) => {
const { t } = useTranslation();
const theme = useTheme();
if (!packageMeta) {
return null;
}
@ -37,8 +41,13 @@ const Author: FC<{ packageMeta }> = ({ packageMeta }) => {
}
const { email, name } = author;
const avatarComponent = <Avatar alt={author.name} src={author.avatar} />;
const avatarComponent = (
<Avatar
alt={author.name}
src={author.avatar}
sx={{ width: 40, height: 40, marginRight: theme.spacing(1) }}
/>
);
return (
<List subheader={<StyledText variant={'subtitle1'}>{t('sidebar.author.title')}</StyledText>}>
@ -50,7 +59,7 @@ const Author: FC<{ packageMeta }> = ({ packageMeta }) => {
{avatarComponent}
</a>
)}
{name && <AuthorListItemText primary={getAuthorName(name)} />}
{name && <Typography variant="subtitle2">{getAuthorName(name)}</Typography>}
</AuthorListItem>
</List>
);

View file

@ -95,6 +95,9 @@ exports[`<Author /> component should render the component in default state 1`] =
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 40px;
height: 40px;
margin-right: 8px;
}
.emotion-6 {
@ -106,24 +109,12 @@ exports[`<Author /> component should render the component in default state 1`] =
text-indent: 10000px;
}
.emotion-8 {
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
min-width: 0;
margin-top: 4px;
margin-bottom: 4px;
padding: 0 10px;
margin: 0;
}
.emotion-9 {
.emotion-7 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
font-size: 1rem;
line-height: 1.5;
display: block;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.57;
}
<body>
@ -153,15 +144,11 @@ exports[`<Author /> component should render the component in default state 1`] =
/>
</div>
</a>
<div
class="MuiListItemText-root emotion-7 emotion-8"
<h6
class="MuiTypography-root MuiTypography-subtitle2 emotion-7"
>
<span
class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary emotion-9"
>
verdaccio user
</span>
</div>
verdaccio user
</h6>
</li>
</ul>
</div>
@ -258,6 +245,9 @@ exports[`<Author /> component should render the component in default state 1`] =
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 40px;
height: 40px;
margin-right: 8px;
}
.emotion-6 {
@ -269,24 +259,12 @@ exports[`<Author /> component should render the component in default state 1`] =
text-indent: 10000px;
}
.emotion-8 {
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
min-width: 0;
margin-top: 4px;
margin-bottom: 4px;
padding: 0 10px;
margin: 0;
}
.emotion-9 {
.emotion-7 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
font-size: 1rem;
line-height: 1.5;
display: block;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.57;
}
<div>
@ -315,15 +293,11 @@ exports[`<Author /> component should render the component in default state 1`] =
/>
</div>
</a>
<div
class="MuiListItemText-root emotion-7 emotion-8"
<h6
class="MuiTypography-root MuiTypography-subtitle2 emotion-7"
>
<span
class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary emotion-9"
>
verdaccio user
</span>
</div>
verdaccio user
</h6>
</li>
</ul>
</div>,
@ -476,6 +450,9 @@ exports[`<Author /> component should render the component when there is no autho
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 40px;
height: 40px;
margin-right: 8px;
}
.emotion-6 {
@ -487,24 +464,12 @@ exports[`<Author /> component should render the component when there is no autho
text-indent: 10000px;
}
.emotion-8 {
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
min-width: 0;
margin-top: 4px;
margin-bottom: 4px;
padding: 0 10px;
margin: 0;
}
.emotion-9 {
.emotion-7 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
font-size: 1rem;
line-height: 1.5;
display: block;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.57;
}
<body>
@ -529,15 +494,11 @@ exports[`<Author /> component should render the component when there is no autho
src="https://www.gravatar.com/avatar/000000"
/>
</div>
<div
class="MuiListItemText-root emotion-7 emotion-8"
<h6
class="MuiTypography-root MuiTypography-subtitle2 emotion-7"
>
<span
class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary emotion-9"
>
verdaccio user
</span>
</div>
verdaccio user
</h6>
</li>
</ul>
</div>
@ -634,6 +595,9 @@ exports[`<Author /> component should render the component when there is no autho
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 40px;
height: 40px;
margin-right: 8px;
}
.emotion-6 {
@ -645,24 +609,12 @@ exports[`<Author /> component should render the component when there is no autho
text-indent: 10000px;
}
.emotion-8 {
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
min-width: 0;
margin-top: 4px;
margin-bottom: 4px;
padding: 0 10px;
margin: 0;
}
.emotion-9 {
.emotion-7 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
font-size: 1rem;
line-height: 1.5;
display: block;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.57;
}
<div>
@ -686,15 +638,11 @@ exports[`<Author /> component should render the component when there is no autho
src="https://www.gravatar.com/avatar/000000"
/>
</div>
<div
class="MuiListItemText-root emotion-7 emotion-8"
<h6
class="MuiTypography-root MuiTypography-subtitle2 emotion-7"
>
<span
class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary emotion-9"
>
verdaccio user
</span>
</div>
verdaccio user
</h6>
</li>
</ul>
</div>,

View file

@ -1,7 +1,6 @@
import styled from '@emotion/styled';
import { Typography } from '@mui/material';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import { Theme } from '../../Theme';
@ -15,8 +14,3 @@ export const AuthorListItem = styled(ListItem)({
backgroundColor: 'transparent',
},
});
export const AuthorListItemText = styled(ListItemText)({
padding: '0 10px',
margin: 0,
});

View file

@ -4,6 +4,7 @@ import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import React from 'react';
import { Theme } from '../../Theme';
import { copyToClipBoardUtility } from './utils';
interface Props {
@ -19,14 +20,14 @@ const Wrapper = styled('div')({
justifyContent: 'space-between',
});
const Content = styled('span')({
const Content = styled('span')<{ theme?: Theme }>(({ theme }) => ({
display: 'inline-block',
overflow: 'hidden',
textOverflow: 'ellipsis',
height: 'auto',
whiteSpace: 'break-spaces',
fontSize: '1rem',
});
fontSize: theme?.fontSize.sm,
}));
function CopyToClipBoard({ text, children, dataTestId, title, ...props }: Props) {
return (

View file

@ -1,30 +1,17 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import { styled } from '@mui/system';
import Alert from '@mui/material/Alert';
import { useTheme } from '@mui/styles';
import React from 'react';
import { Theme } from '../../Theme';
export const CardStyled = styled(Card)<{ theme?: Theme }>(({ theme }) => {
return {
marginTop: theme?.spacing(1),
marginBottom: theme?.spacing(0.5),
backgroundColor: theme?.palette?.error.light,
opacity: '0.9',
color: theme?.palette?.error.contrastText,
fontWeight: 'bold',
};
});
export type Props = {
message: string;
};
const Deprecated: React.FC<Props> = ({ message }) => {
const theme = useTheme();
return (
<CardStyled>
<CardContent>{message}</CardContent>
</CardStyled>
<Alert severity="warning" sx={{ marginTop: theme.spacing(1), marginBottom: theme.spacing(1) }}>
{message}
</Alert>
);
};

View file

@ -1,7 +1,7 @@
import { Typography } from '@mui/material';
import Avatar from '@mui/material/Avatar';
import Grid from '@mui/material/Grid';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
@ -24,7 +24,7 @@ const EngineItem: FC<EngineItemProps> = ({ title, element, engineText }) => (
<List subheader={<StyledText variant={'subtitle1'}>{title}</StyledText>}>
<EngineListItem>
<Avatar sx={{ bgcolor: '#FFF' }}>{element}</Avatar>
<ListItemText primary={engineText} />
<Typography variant="subtitle2">{engineText}</Typography>
</EngineListItem>
</List>
</Grid>

View file

@ -69,7 +69,7 @@ exports[`<Help /> component should load the component in default state 1`] = `
text-overflow: ellipsis;
height: auto;
white-space: break-spaces;
font-size: 1rem;
font-size: 14px;
}
.emotion-10 {

View file

@ -5,6 +5,7 @@ import { PackageManagers } from '@verdaccio/types';
import { useConfig } from '../../providers';
import { render, screen } from '../../test/test-react-testing-library';
import Install from './Install';
import { getGlobalInstall } from './InstallListItem';
import data from './__partials__/data.json';
const ComponentToBeRendered: React.FC<{ pkgManagers?: PackageManagers[] }> = () => {
@ -16,12 +17,9 @@ const ComponentToBeRendered: React.FC<{ pkgManagers?: PackageManagers[] }> = ()
describe('<Install />', () => {
test('renders correctly', () => {
render(<ComponentToBeRendered />);
expect(screen.getByText('sidebar.installation.install-using-pnpm')).toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-yarn')).toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-npm')).toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-npm-command')).toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-yarn-command')).toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-pnpm-command')).toBeInTheDocument();
expect(screen.getByText('yarn add foo@8.0.0')).toBeInTheDocument();
expect(screen.getByText('pnpm install foo@8.0.0')).toBeInTheDocument();
expect(screen.getByText('npm install foo@8.0.0')).toBeInTheDocument();
});
test('should have 3 children', () => {
@ -38,47 +36,41 @@ describe('<Install />', () => {
render(<ComponentToBeRendered />);
expect(screen.getByText('sidebar.installation.title')).toBeTruthy();
expect(screen.queryByText('sidebar.installation.install-using-pnpm')).not.toBeInTheDocument();
expect(screen.queryByText('sidebar.installation.install-using-yarn')).not.toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-npm')).toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-npm-command')).toBeInTheDocument();
expect(
screen.queryByText('sidebar.installation.install-using-yarn-command')
).not.toBeInTheDocument();
expect(
screen.queryByText('sidebar.installation.install-using-pnpm-command')
).not.toBeInTheDocument();
expect(screen.queryByText('pnpm')).not.toBeInTheDocument();
expect(screen.queryByText('yarn')).not.toBeInTheDocument();
expect(screen.getByText('npm install foo@8.0.0')).toBeInTheDocument();
});
test('should have the element YARN', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['yarn'];
render(<ComponentToBeRendered />);
expect(screen.getByText('sidebar.installation.title')).toBeTruthy();
expect(screen.queryByText('sidebar.installation.install-using-pnpm')).not.toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-yarn')).toBeInTheDocument();
expect(screen.queryByText('sidebar.installation.install-using-npm')).not.toBeInTheDocument();
expect(
screen.queryByText('sidebar.installation.install-using-npm-command')
).not.toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-yarn-command')).toBeInTheDocument();
expect(
screen.queryByText('sidebar.installation.install-using-pnpm-command')
).not.toBeInTheDocument();
expect(screen.queryByText('pnpm')).not.toBeInTheDocument();
expect(screen.queryByText('npm')).not.toBeInTheDocument();
expect(screen.getByText('yarn add foo@8.0.0')).toBeInTheDocument();
});
test('should have the element PNPM', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['pnpm'];
render(<ComponentToBeRendered />);
expect(screen.getByText('sidebar.installation.title')).toBeTruthy();
expect(screen.getByText('sidebar.installation.install-using-pnpm')).toBeInTheDocument();
expect(screen.queryByText('sidebar.installation.install-using-yarn')).not.toBeInTheDocument();
expect(screen.queryByText('sidebar.installation.install-using-npm')).not.toBeInTheDocument();
expect(
screen.queryByText('sidebar.installation.install-using-npm-command')
).not.toBeInTheDocument();
expect(
screen.queryByText('sidebar.installation.install-using-yarn-command')
).not.toBeInTheDocument();
expect(screen.getByText('sidebar.installation.install-using-pnpm-command')).toBeInTheDocument();
expect(screen.queryByText('pnpm')).not.toBeInTheDocument();
expect(screen.queryByText('yarn')).not.toBeInTheDocument();
expect(screen.getByText('pnpm install foo@8.0.0')).toBeInTheDocument();
});
});
describe('getGlobalInstall', () => {
test('no global', () => {
expect(getGlobalInstall(false, 'foo', '1.0.0')).toEqual('1.0.0@foo');
});
test('global', () => {
expect(getGlobalInstall(true, 'foo', '1.0.0')).toEqual('-g 1.0.0@foo');
});
test('yarn no global', () => {
expect(getGlobalInstall(false, 'foo', '1.0.0', true)).toEqual('1.0.0@foo');
});
test('yarn global', () => {
expect(getGlobalInstall(true, 'foo', '1.0.0', true)).toEqual('1.0.0@foo');
});
});

View file

@ -27,7 +27,6 @@ export type Props = {
const Install: React.FC<Props> = ({ packageMeta, packageName, configOptions }) => {
const { t } = useTranslation();
const theme = useTheme();
if (!packageMeta || !packageName) {
return null;
}
@ -51,13 +50,25 @@ const Install: React.FC<Props> = ({ packageMeta, packageName, configOptions }) =
subheader={<StyledText variant={'subtitle1'}>{t('sidebar.installation.title')}</StyledText>}
>
{hasNpm && (
<InstallListItem dependencyManager={DependencyManager.NPM} packageName={packageName} />
<InstallListItem
dependencyManager={DependencyManager.NPM}
packageName={packageName}
packageVersion={packageMeta.latest.version}
/>
)}
{hasYarn && (
<InstallListItem dependencyManager={DependencyManager.YARN} packageName={packageName} />
<InstallListItem
dependencyManager={DependencyManager.YARN}
packageName={packageName}
packageVersion={packageMeta.latest.version}
/>
)}
{hasPnpm && (
<InstallListItem dependencyManager={DependencyManager.PNPM} packageName={packageName} />
<InstallListItem
dependencyManager={DependencyManager.PNPM}
packageName={packageName}
packageVersion={packageMeta.latest.version}
/>
)}
</List>
</>

View file

@ -2,8 +2,8 @@ import styled from '@emotion/styled';
import Avatar from '@mui/material/Avatar';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import { useTheme } from '@mui/styles';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSettings } from '../../providers/PersistenceSettingProvider';
import CopyToClipBoard from '../CopyClipboard';
@ -35,78 +35,94 @@ export enum DependencyManager {
interface Interface {
packageName: string;
dependencyManager: DependencyManager;
packageVersion?: string;
}
const InstallListItem: React.FC<Interface> = ({ packageName, dependencyManager }) => {
const { t } = useTranslation();
const { localSettings } = useSettings();
const isGlobal = localSettings[packageName]?.global ?? false;
const pkgName = isGlobal ? `-g ${packageName}` : packageName;
export function getGlobalInstall(isGlobal, packageVersion, packageName, isYarn = false) {
const name = isGlobal
? `${isYarn ? '' : '-g'} ${packageVersion ? `${packageName}@${packageVersion}` : packageName}`
: packageVersion
? `${packageName}@${packageVersion}`
: packageName;
return name.trim();
}
const InstallListItem: React.FC<Interface> = ({
packageName,
dependencyManager,
packageVersion,
}) => {
const { localSettings } = useSettings();
const theme = useTheme();
const isGlobal = localSettings[packageName]?.global ?? false;
switch (dependencyManager) {
case DependencyManager.NPM:
return (
<InstallItem data-testid={'installListItem-npm'}>
<PackageMangerAvatar alt="npm" sx={{ bgcolor: '#FFF' }}>
<PackageMangerAvatar alt="npm" sx={{ bgcolor: theme.palette.white }}>
<Npm />
</PackageMangerAvatar>
<InstallListItemText
primary={
<CopyToClipBoard
dataTestId="installYarn"
text={t('sidebar.installation.install-using-npm-command', {
packageName: pkgName,
})}
title={t('sidebar.installation.install-using-npm-command', {
packageName: pkgName,
})}
dataTestId="instalNpm"
text={`npm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
title={`npm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
/>
}
secondary={t('sidebar.installation.install-using-npm')}
/>
</InstallItem>
);
case DependencyManager.YARN:
return (
<InstallItem data-testid={'installListItem-yarn'}>
<PackageMangerAvatar alt="yarn" sx={{ bgcolor: '#FFF' }}>
<PackageMangerAvatar alt="yarn" sx={{ bgcolor: theme.palette.white }}>
<Yarn />
</PackageMangerAvatar>
<InstallListItemText
primary={
<CopyToClipBoard
dataTestId="installYarn"
text={t('sidebar.installation.install-using-yarn-command', {
packageName: pkgName,
})}
title={t('sidebar.installation.install-using-yarn-command', {
packageName: pkgName,
})}
text={
isGlobal
? `yarn ${localSettings.yarnModern ? '' : 'global'} add ${getGlobalInstall(
isGlobal,
packageVersion,
packageName,
true
)}`
: `yarn add ${getGlobalInstall(isGlobal, packageVersion, packageName, true)}`
}
title={
isGlobal
? `yarn global add ${getGlobalInstall(
isGlobal,
packageVersion,
packageName,
true
)}`
: `yarn add ${getGlobalInstall(isGlobal, packageVersion, packageName, true)}`
}
/>
}
secondary={t('sidebar.installation.install-using-yarn')}
/>
</InstallItem>
);
case DependencyManager.PNPM:
return (
<InstallItem data-testid={'installListItem-pnpm'}>
<PackageMangerAvatar alt={'pnpm'} sx={{ bgcolor: '#FFF' }}>
<PackageMangerAvatar alt={'pnpm'} sx={{ bgcolor: theme.palette.white }}>
<Pnpm />
</PackageMangerAvatar>
<InstallListItemText
primary={
<CopyToClipBoard
dataTestId="installPnpm"
text={t('sidebar.installation.install-using-pnpm-command', {
packageName: pkgName,
})}
title={t('sidebar.installation.install-using-pnpm-command', {
packageName: pkgName,
})}
text={`pnpm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
title={`pnpm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
/>
}
secondary={t('sidebar.installation.install-using-pnpm')}
/>
</InstallItem>
);

View file

@ -4,6 +4,7 @@ import BugReport from '@mui/icons-material/BugReport';
import DownloadIcon from '@mui/icons-material/CloudDownload';
import HomeIcon from '@mui/icons-material/Home';
import { useTheme } from '@mui/material';
import CircularProgress from '@mui/material/CircularProgress';
import Grid from '@mui/material/Grid';
import ListItem from '@mui/material/ListItem';
import Tooltip from '@mui/material/Tooltip';
@ -71,6 +72,7 @@ const Package: React.FC<PackageInterface> = ({
const dispatch = useDispatch<Dispatch>();
const { t } = useTranslation();
const theme = useTheme();
const isLoading = useSelector((state: RootState) => state?.loading?.models.download);
const handleDownload = useCallback(
async (tarballDist: string) => {
@ -173,7 +175,13 @@ const Package: React.FC<PackageInterface> = ({
title={t('package.tarball')}
>
<IconButton aria-label={t('package.download')} size="large">
<DownloadIcon />
{isLoading ? (
<CircularProgress size={13}>
<DownloadIcon />
</CircularProgress>
) : (
<DownloadIcon />
)}
</IconButton>
</Tooltip>
</Link>
@ -194,6 +202,7 @@ const Package: React.FC<PackageInterface> = ({
container={true}
item={true}
justify="flex-end"
spacing={3}
xs={true}
>
{renderHomePageLink()}

View file

@ -47,12 +47,7 @@ type Props = {
const RawViewer: React.FC<Props> = ({ isOpen = false, onClose, packageMeta }) => {
const { t } = useTranslation();
return (
<Dialog
data-testid={'rawViewer--dialog'}
fullScreen={true}
id="raw-viewer--dialog-container"
open={isOpen}
>
<Dialog data-testid={'rawViewer--dialog'} fullScreen={true} open={isOpen}>
<ViewerTitle id="viewer-title" onClose={onClose}>
{t('action-bar-action.raw')}
</ViewerTitle>

View file

@ -1,30 +1,19 @@
import styled from '@emotion/styled';
import 'github-markdown-css';
import 'highlight.js/styles/default.css';
import React from 'react';
import { Theme } from '../../Theme';
import { useCustomTheme } from '../../';
import ReadmeDark from './ReadmeDark';
import ReadmeLight from './ReadmeLight';
import { Props } from './types';
import { parseReadme } from './utils';
const Readme: React.FC<Props> = ({ description }) => {
return (
<Wrapper
className="markdown-body"
dangerouslySetInnerHTML={{ __html: parseReadme(description) as string }}
/>
// @ts-ignore
const { isDarkMode } = useCustomTheme();
return isDarkMode ? (
<ReadmeDark description={description} />
) : (
<ReadmeLight description={description} />
);
};
export default Readme;
const Wrapper = styled('div')<{ theme?: Theme }>(({ theme }) => ({
background: theme?.palette.white,
color: theme?.palette.black,
padding: theme?.spacing(2, 3),
ul: {
listStyle: 'disc',
},
img: {
maxWidth: '100%',
},
}));

View file

@ -0,0 +1,32 @@
import styled from '@emotion/styled';
import cx from 'classnames';
import 'highlight.js/styles/default.css';
import React from 'react';
import { Theme } from '../../Theme';
// @ts-ignore
import styles from './github-markdown-dark.css';
import { Props } from './types';
import { parseReadme } from './utils';
const Readme: React.FC<Props> = ({ description }) => {
return (
<Wrapper
className={cx('markdown-body', styles['markdown-body'])}
dangerouslySetInnerHTML={{ __html: parseReadme(description) as string }}
/>
);
};
export default Readme;
const Wrapper = styled('div')<{ theme?: Theme }>(({ theme }) => ({
background: theme?.palette.white,
color: theme?.palette.black,
padding: theme?.spacing(2, 3),
ul: {
listStyle: 'disc',
},
img: {
maxWidth: '100%',
},
}));

View file

@ -0,0 +1,32 @@
import styled from '@emotion/styled';
import cx from 'classnames';
import 'highlight.js/styles/default.css';
import React from 'react';
import { Theme } from '../../Theme';
// @ts-ignore
import styles from './github-markdown-light.css';
import { Props } from './types';
import { parseReadme } from './utils';
const Readme: React.FC<Props> = ({ description }) => {
return (
<Wrapper
className={cx('markdown-body', styles['markdown-body'])}
dangerouslySetInnerHTML={{ __html: parseReadme(description) as string }}
/>
);
};
export default Readme;
const Wrapper = styled('div')<{ theme?: Theme }>(({ theme }) => ({
background: theme?.palette.white,
color: theme?.palette.black,
padding: theme?.spacing(2, 3),
ul: {
listStyle: 'disc',
},
img: {
maxWidth: '100%',
},
}));

View file

@ -20,7 +20,7 @@ exports[`<Readme /> component should load the component in default state 1`] = `
<body>
<div>
<div
class="markdown-body emotion-0 emotion-1"
class="markdown-body markdown-body emotion-0 emotion-1"
>
<p>
test
@ -46,7 +46,7 @@ exports[`<Readme /> component should load the component in default state 1`] = `
<div>
<div
class="markdown-body emotion-0 emotion-1"
class="markdown-body markdown-body emotion-0 emotion-1"
>
<p>
test

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -21,14 +21,24 @@ const InstallListItem: React.FC<Props> = ({ packageName }) => {
const handleOpenMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClick = () => {
const handleGlobalSelect = () => {
const statusGlobal = !localSettings[packageName]?.global;
updateSettings({ [packageName]: { global: statusGlobal } });
updateSettings({ ...localSettings, [packageName]: { global: statusGlobal } });
setAnchorEl(null);
};
const handleGlobalYarnModern = () => {
const statusYarnModern = !localSettings?.yarnModern;
updateSettings({ ...localSettings, yarnModern: statusYarnModern });
setAnchorEl(null);
};
const handleClose = () => {
setAnchorEl(null);
};
const statusGlobal = localSettings[packageName]?.global;
return (
<>
<IconButton
@ -50,15 +60,24 @@ const InstallListItem: React.FC<Props> = ({ packageName }) => {
onClose={handleClose}
open={open}
>
<MenuItem onClick={handleClick}>
<MenuItem onClick={handleGlobalSelect}>
{' '}
{localSettings?.global ? (
{statusGlobal === true ? (
<ListItemIcon>
<Check />
</ListItemIcon>
) : null}
{t('sidebar.installation.global')}
</MenuItem>
<MenuItem onClick={handleGlobalYarnModern}>
{' '}
{localSettings?.yarnModern ? (
<ListItemIcon>
<Check />
</ListItemIcon>
) : null}
{t('sidebar.installation.yarnModern')}
</MenuItem>
</Menu>
</>
);

View file

@ -78,4 +78,5 @@ const StyledHeading = styled(Heading)({
const StyledBoxVersion = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
color: theme?.palette.text.secondary,
fontSize: theme?.fontSize.sm,
}));

View file

@ -1,8 +1,11 @@
import Chip from '@mui/material/Chip';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import { useTheme } from '@mui/styles';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../providers';
import { Time, Versions } from '../../types/packageMeta';
import { utils } from '../../utils';
import { Link } from '../Link';
@ -14,18 +17,43 @@ interface Props {
time: Time;
}
export function filterDeprecated(versions: Versions) {
const versionsIds = Object.keys(versions);
return versionsIds.reduce((prev, current) => {
if (!versions[current].deprecated) {
prev[current] = versions[current];
}
return prev;
}, {});
}
const VersionsHistoryList: React.FC<Props> = ({ versions, packageName, time }) => {
const { t } = useTranslation();
const { configOptions } = useConfig();
const theme = useTheme();
const hideDeprecatedVersions = configOptions.hideDeprecatedVersions;
const listVersions = hideDeprecatedVersions ? filterDeprecated(versions) : versions;
return (
<List dense={true}>
{Object.keys(versions)
{Object.keys(listVersions)
.reverse()
.map((version) => (
<ListItem className="version-item" data-testid={`version-${version}`} key={version}>
<Link to={`/-/web/detail/${packageName}/v/${version}`} variant="caption">
<ListItemText disableTypography={false} primary={version}></ListItemText>
</Link>
{typeof versions[version].deprecated === 'string' ? (
<Chip
color="warning"
data-testid="deprecated-badge"
label={t('versions.deprecated')}
size="small"
sx={{ marginLeft: theme.spacing(1) }}
variant="outlined"
/>
) : null}
<Spacer />
<ListItemText title={utils.formatDate(time[version])}>
{time[version]

View file

@ -2,9 +2,10 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { cleanup, render } from '../../test/test-react-testing-library';
import { cleanup, render, screen } from '../../test/test-react-testing-library';
import Versions, { Props } from './Versions';
import data from './__partials__/data.json';
import dataDeprecated from './__partials__/deprecated-versions.json';
const ComponentToBeRendered: React.FC<Props> = (props) => (
<MemoryRouter>
@ -26,6 +27,8 @@ describe('<Version /> component', () => {
// pick some versions
expect(getByText('2.3.0')).toBeTruthy();
expect(getByText('canary')).toBeTruthy();
// there is one deprecated version deprecated
expect(screen.queryByTestId('deprecated-badge')).toBeInTheDocument();
});
test('should not render versions', () => {
@ -35,5 +38,17 @@ describe('<Version /> component', () => {
expect(queryByText('versions.current-tags')).toBeFalsy();
});
test('should render versions deprecated settings', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS.hideDeprecatedVersions = true;
const { getByText } = render(
<ComponentToBeRendered packageMeta={dataDeprecated} packageName={'foo'} />
);
expect(getByText('versions.hide-deprecated')).toBeTruthy();
// pick some versions
expect(screen.queryByText('0.0.2')).not.toBeInTheDocument();
expect(screen.getByText('0.0.1')).toBeInTheDocument();
});
test.todo('should click on version link');
});

View file

@ -1,7 +1,10 @@
import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography';
import { useTheme } from '@mui/styles';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../providers';
import VersionsHistoryList from './HistoryList';
import VersionsTagList from './TagList';
@ -9,6 +12,8 @@ export type Props = { packageMeta: any; packageName: string };
const Versions: React.FC<Props> = ({ packageMeta, packageName }) => {
const { t } = useTranslation();
const { configOptions } = useConfig();
const theme = useTheme();
if (!packageMeta) {
return null;
@ -18,6 +23,7 @@ const Versions: React.FC<Props> = ({ packageMeta, packageName }) => {
const hasDistTags = distTags && Object.keys(distTags).length > 0 && packageName;
const hasVersionHistory = versions && Object.keys(versions).length > 0 && packageName;
const hideDeprecatedVersions = configOptions.hideDeprecatedVersions;
return (
<>
@ -30,7 +36,17 @@ const Versions: React.FC<Props> = ({ packageMeta, packageName }) => {
{hasVersionHistory ? (
<>
<Typography variant="subtitle1">{t('versions.version-history')}</Typography>
<VersionsHistoryList packageName={packageName} time={time} versions={versions} />
<>
{hideDeprecatedVersions && (
<Alert
severity="info"
sx={{ marginTop: theme.spacing(1), marginBottom: theme.spacing(1) }}
>
{t('versions.hide-deprecated')}
</Alert>
)}
<VersionsHistoryList packageName={packageName} time={time} versions={versions} />
</>
</>
) : null}
</>

View file

@ -3,6 +3,7 @@
"0.0.1": {
"name": "@verdaccio/local-storage",
"version": "0.0.1",
"deprecated": "this is deprecated",
"description": "local storage implementation",
"main": "lib/index.js",
"scripts": {

View file

@ -0,0 +1,175 @@
{
"versions": {
"0.0.1": {
"name": "@verdaccio/local-storage",
"version": "0.0.1",
"description": "local storage implementation",
"main": "lib/index.js",
"scripts": {
"test": "npm run lint && mocha --require babel-polyfill --compilers js:babel-core/register ./test/**/*.spec.js",
"lint": "eslint .",
"flow": "flow",
"build": "babel src/ --out-dir lib/ --copy-files",
"cover": "cross-env NODE_ENV=test nyc npm t"
},
"dependencies": {
"@verdaccio/file-locking": "^0.0.3",
"@verdaccio/streams": "^0.0.2",
"async": "2.5.0",
"http-errors": "1.4.0",
"lodash": "4.17.4",
"mkdirp": "0.5.1"
},
"devDependencies": {
"@verdaccio/types": "0.0.2",
"babel-cli": "6.24.1",
"babel-core": "6.25.0",
"babel-eslint": "^7.2.3",
"babel-plugin-istanbul": "4.1.4",
"babel-polyfill": "6.23.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-es2015-node4": "2.1.0",
"babel-preset-flow": "6.23.0",
"babel-plugin-transform-inline-imports-commonjs": "1.0.0",
"babel-plugin-array-includes": "2.0.3",
"babel-plugin-transform-runtime": "6.4.3",
"cross-env": "5.0.5",
"eslint": "4.4.1",
"eslint-config-google": "0.9.1",
"eslint-plugin-flowtype": "2.35.0",
"flow-bin": "0.52.0",
"mocha": "3.5.0",
"nyc": "11.1.0"
},
"nyc": {
"include": ["src/**/*.js"],
"all": true,
"cache": true,
"sourceMap": false,
"instrument": false,
"report-dir": "./tests-report",
"reporter": ["text", "html"]
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"keywords": ["streams"],
"author": {
"name": "Juan Picado",
"email": "juanpicado19@gmail.com"
},
"private": false,
"license": "MIT",
"gitHead": "e7e4ea74f20331dfde0a7133a02b5b95d68696a9",
"_id": "@verdaccio/local-storage@0.0.1",
"_npmVersion": "5.3.0",
"_nodeVersion": "8.2.1",
"_npmUser": {
"name": "jotadeveloper",
"email": "juanpicado19@gmail.com"
},
"dist": {
"integrity": "sha512-J7qeK3r6jbaMOf4mBIqL1bAdb7Iwb+mC4ZaE9PUE2TctzXSkRRnk2M33zWGhZV0ziEPGAF40wC76wWex2GvXeA==",
"shasum": "9c71cbeeb922b35efc1b31949d0afa6aac97e9b6",
"tarball": "http://localhost:4872/@verdaccio%2flocal-storage/-/local-storage-0.0.1.tgz"
},
"maintainers": [
{
"name": "jotadeveloper",
"email": "juanpicado19@gmail.com"
}
],
"_npmOperationalInternal": {
"host": "s3://npm-registry-packages",
"tmp": "tmp/local-storage-0.0.1.tgz_1502536792670_0.3513342353980988"
},
"directories": {}
},
"0.0.2": {
"deprecated": "this package is deprecated",
"name": "@verdaccio/local-storage",
"version": "0.0.2",
"description": "local storage implementation",
"main": "lib/index.js",
"scripts": {
"test": "npm run lint && mocha --require babel-polyfill --compilers js:babel-core/register ./test/**/*.spec.js",
"lint": "eslint .",
"flow": "flow",
"build": "babel src/ --out-dir lib/ --copy-files",
"cover": "cross-env NODE_ENV=test nyc npm t"
},
"dependencies": {
"@verdaccio/file-locking": "^0.0.3",
"@verdaccio/streams": "^0.0.2",
"async": "2.5.0",
"http-errors": "1.4.0",
"lodash": "4.17.4",
"mkdirp": "0.5.1"
},
"devDependencies": {
"@verdaccio/types": "0.0.2",
"babel-cli": "6.24.1",
"babel-core": "6.25.0",
"babel-eslint": "^7.2.3",
"babel-plugin-istanbul": "4.1.4",
"babel-polyfill": "6.23.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-es2015-node4": "2.1.0",
"babel-preset-flow": "6.23.0",
"babel-plugin-transform-inline-imports-commonjs": "1.0.0",
"babel-plugin-array-includes": "2.0.3",
"babel-plugin-transform-runtime": "6.4.3",
"cross-env": "5.0.5",
"eslint": "4.4.1",
"eslint-config-google": "0.9.1",
"eslint-plugin-flowtype": "2.35.0",
"flow-bin": "0.52.0",
"mocha": "3.5.0",
"nyc": "11.1.0"
},
"nyc": {
"include": ["src/**/*.js"],
"all": true,
"cache": true,
"sourceMap": false,
"instrument": false,
"report-dir": "./tests-report",
"reporter": ["text", "html"]
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"keywords": ["streams"],
"author": {
"name": "Juan Picado",
"email": "juanpicado19@gmail.com"
},
"private": false,
"license": "MIT",
"gitHead": "626ea013ee810da9115550e0a60f44aa3d08e9a0",
"_id": "@verdaccio/local-storage@0.0.2",
"_npmVersion": "5.3.0",
"_nodeVersion": "8.2.1",
"_npmUser": {
"name": "jotadeveloper",
"email": "juanpicado19@gmail.com"
},
"dist": {
"integrity": "sha512-Su0cQpLAaBab3qekpcHO+J/JCfoznTuU0P7e5q9T7R2trGVobYXvsTiIB/usQNmDr2e+1N6iBhMst7Uy+231ag==",
"shasum": "fe79ad6f9ceb631857618038486f729098e7b31b",
"tarball": "http://localhost:4872/@verdaccio%2flocal-storage/-/local-storage-0.0.2.tgz"
},
"maintainers": [
{
"name": "jotadeveloper",
"email": "juanpicado19@gmail.com"
}
],
"_npmOperationalInternal": {
"host": "s3://npm-registry-packages",
"tmp": "tmp/local-storage-0.0.2.tgz_1502611253150_0.21545960218645632"
},
"directories": {}
}
}
}

View file

@ -28,6 +28,7 @@ const defaultValues: ConfigProviderProps = {
showSearch: true,
showRaw: true,
showDownloadTarball: true,
hideDeprecatedVersions: false,
title: 'Verdaccio',
},
};

View file

@ -11,10 +11,12 @@ import useLocalStorage from '../../hooks/useLocalStorage';
type PersistenceSettingsProps = {
isGlobal?: boolean;
yarnModern: boolean;
};
const defaultValues: PersistenceSettingsProps = {
isGlobal: false,
yarnModern: false,
};
const PersistenceSettingContext = createContext<any>(defaultValues);

View file

@ -13,7 +13,6 @@ export const download = createModel<RootModel>()({
reducers: {},
effects: () => ({
async getTarball({ link }) {
// const basePath = state.configuration.config.base;
try {
const fileStream: Blob = await API.request(link, 'GET', {
headers: {

View file

@ -78,6 +78,7 @@ export interface Version {
license?: string;
main?: string;
keywords?: string[];
deprecated?: string;
}
export interface Author {

790
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff