mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-30 22:34:10 -05:00
feat: versions filter by semver range (#4555)
* fix: improve dark mode styles * fix background color * feat: ui improvements * add tests
This commit is contained in:
parent
a99a4bb1b3
commit
ba53d1edc8
15 changed files with 93 additions and 76 deletions
6
.changeset/eighty-lobsters-study.md
Normal file
6
.changeset/eighty-lobsters-study.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'@verdaccio/ui-theme': patch
|
||||||
|
'@verdaccio/ui-components': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: versions filter by semver range
|
|
@ -55,6 +55,7 @@
|
||||||
"version-history": "Version History",
|
"version-history": "Version History",
|
||||||
"not-available": "Not available",
|
"not-available": "Not available",
|
||||||
"deprecated": "Deprecated",
|
"deprecated": "Deprecated",
|
||||||
|
"search.placeholder": "Search for version by semver range, e.g. ^1.0.0",
|
||||||
"hide-deprecated": "All deprecated versions are hidden by global configuration"
|
"hide-deprecated": "All deprecated versions are hidden by global configuration"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
|
|
|
@ -35,7 +35,7 @@ module.exports = Object.assign({}, config, {
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
branches: 70,
|
branches: 70,
|
||||||
functions: 76,
|
functions: 75,
|
||||||
lines: 80,
|
lines: 80,
|
||||||
statements: 81,
|
statements: 81,
|
||||||
},
|
},
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
"react-redux": "8.1.3",
|
"react-redux": "8.1.3",
|
||||||
"react-router": "5.3.4",
|
"react-router": "5.3.4",
|
||||||
"react-router-dom": "5.3.4",
|
"react-router-dom": "5.3.4",
|
||||||
|
"semver": "7.6.0",
|
||||||
"react-virtualized": "9.22.5",
|
"react-virtualized": "9.22.5",
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
"validator": "13.11.0"
|
"validator": "13.11.0"
|
||||||
|
|
|
@ -36,7 +36,7 @@ const themeModes = {
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
...colors,
|
...colors,
|
||||||
primary: '#24394e',
|
primary: '#ffffff',
|
||||||
secondary: '#424242',
|
secondary: '#424242',
|
||||||
background: '#1A202C',
|
background: '#1A202C',
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,7 +23,7 @@ const EngineItem: FC<EngineItemProps> = ({ title, element, engineText }) => (
|
||||||
<Grid item={true} xs={6}>
|
<Grid item={true} xs={6}>
|
||||||
<List subheader={<StyledText variant={'subtitle1'}>{title}</StyledText>}>
|
<List subheader={<StyledText variant={'subtitle1'}>{title}</StyledText>}>
|
||||||
<EngineListItem>
|
<EngineListItem>
|
||||||
<Avatar sx={{ bgcolor: '#FFF' }}>{element}</Avatar>
|
<Avatar sx={{ bgcolor: 'transparent' }}>{element}</Avatar>
|
||||||
<Typography variant="subtitle2">{engineText}</Typography>
|
<Typography variant="subtitle2">{engineText}</Typography>
|
||||||
</EngineListItem>
|
</EngineListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
|
@ -23,6 +23,7 @@ const InstallListItemText = styled(ListItemText)({
|
||||||
|
|
||||||
const PackageMangerAvatar = styled(Avatar)({
|
const PackageMangerAvatar = styled(Avatar)({
|
||||||
borderRadius: '0px',
|
borderRadius: '0px',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -40,9 +40,7 @@ const RepositoryListItemText = styled(ListItemText)({
|
||||||
const RepositoryAvatar = styled(Avatar)({
|
const RepositoryAvatar = styled(Avatar)({
|
||||||
borderRadius: '0px',
|
borderRadius: '0px',
|
||||||
padding: '0',
|
padding: '0',
|
||||||
img: {
|
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const Repository: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
const Repository: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
||||||
|
|
|
@ -44,7 +44,7 @@ const VersionsHistoryList: React.FC<Props> = ({ versions, packageName, time }) =
|
||||||
<Link to={`/-/web/detail/${packageName}/v/${version}`} variant="caption">
|
<Link to={`/-/web/detail/${packageName}/v/${version}`} variant="caption">
|
||||||
<ListItemText disableTypography={false} primary={version}></ListItemText>
|
<ListItemText disableTypography={false} primary={version}></ListItemText>
|
||||||
</Link>
|
</Link>
|
||||||
{typeof versions[version].deprecated === 'string' ? (
|
{typeof versions[version]?.deprecated === 'string' ? (
|
||||||
<Chip
|
<Chip
|
||||||
color="warning"
|
color="warning"
|
||||||
data-testid="deprecated-badge"
|
data-testid="deprecated-badge"
|
||||||
|
@ -55,7 +55,7 @@ const VersionsHistoryList: React.FC<Props> = ({ versions, packageName, time }) =
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ListItemText title={utils.formatDate(time[version])}>
|
<ListItemText data-testid={`version-list-text`} title={utils.formatDate(time[version])}>
|
||||||
{time[version]
|
{time[version]
|
||||||
? utils.formatDateDistance(time[version])
|
? utils.formatDateDistance(time[version])
|
||||||
: t('versions.not-available')}
|
: t('versions.not-available')}
|
||||||
|
|
|
@ -2,24 +2,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { cleanup, render, screen } from '../../test/test-react-testing-library';
|
import { fireEvent, render, screen } from '../../test/test-react-testing-library';
|
||||||
import Versions, { Props } from './Versions';
|
import Versions, { Props } from './Versions';
|
||||||
import data from './__partials__/data.json';
|
import data from './__partials__/data.json';
|
||||||
import dataDeprecated from './__partials__/deprecated-versions.json';
|
import dataDeprecated from './__partials__/deprecated-versions.json';
|
||||||
|
|
||||||
const ComponentToBeRendered: React.FC<Props> = (props) => (
|
const VersionsComponent: React.FC<Props> = (props) => (
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Versions {...props} />
|
<Versions {...props} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.mock('lodash/debounce', () =>
|
||||||
|
jest.fn((fn) => {
|
||||||
|
fn.cancel = jest.fn();
|
||||||
|
return fn;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
describe('<Version /> component', () => {
|
describe('<Version /> component', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render versions', () => {
|
test('should render versions', () => {
|
||||||
const { getByText } = render(<ComponentToBeRendered packageMeta={data} packageName={'foo'} />);
|
const { getByText } = render(<VersionsComponent packageMeta={data} packageName={'foo'} />);
|
||||||
|
|
||||||
expect(getByText('versions.version-history')).toBeTruthy();
|
expect(getByText('versions.version-history')).toBeTruthy();
|
||||||
expect(getByText('versions.current-tags')).toBeTruthy();
|
expect(getByText('versions.current-tags')).toBeTruthy();
|
||||||
|
@ -31,17 +38,26 @@ describe('<Version /> component', () => {
|
||||||
expect(screen.queryByTestId('deprecated-badge')).toBeInTheDocument();
|
expect(screen.queryByTestId('deprecated-badge')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not render versions', () => {
|
test('should filter by version', () => {
|
||||||
const { queryByText } = render(<ComponentToBeRendered packageMeta={{}} packageName={'foo'} />);
|
render(<VersionsComponent packageMeta={data} packageName={'foo'} />);
|
||||||
|
expect(screen.getByText('versions.version-history')).toBeTruthy();
|
||||||
|
expect(screen.getByText('versions.current-tags')).toBeTruthy();
|
||||||
|
expect(screen.queryAllByTestId('version-list-text')).toHaveLength(65);
|
||||||
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: '2.3.0' } });
|
||||||
|
expect(screen.queryAllByTestId('version-list-text')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
expect(queryByText('versions.version-history')).toBeFalsy();
|
test('should not render versions', () => {
|
||||||
expect(queryByText('versions.current-tags')).toBeFalsy();
|
render(<VersionsComponent packageMeta={{}} packageName={'foo'} />);
|
||||||
|
|
||||||
|
expect(screen.queryByText('versions.version-history')).toBeFalsy();
|
||||||
|
expect(screen.queryByText('versions.current-tags')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render versions deprecated settings', () => {
|
test('should render versions deprecated settings', () => {
|
||||||
window.__VERDACCIO_BASENAME_UI_OPTIONS.hideDeprecatedVersions = true;
|
window.__VERDACCIO_BASENAME_UI_OPTIONS.hideDeprecatedVersions = true;
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<ComponentToBeRendered packageMeta={dataDeprecated} packageName={'foo'} />
|
<VersionsComponent packageMeta={dataDeprecated} packageName={'foo'} />
|
||||||
);
|
);
|
||||||
expect(getByText('versions.hide-deprecated')).toBeTruthy();
|
expect(getByText('versions.hide-deprecated')).toBeTruthy();
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { useTheme } from '@mui/styles';
|
import { useTheme } from '@mui/styles';
|
||||||
import React from 'react';
|
import debounce from 'lodash/debounce';
|
||||||
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import semver from 'semver';
|
||||||
|
|
||||||
import { useConfig } from '../../providers';
|
import { useConfig } from '../../providers';
|
||||||
import VersionsHistoryList from './HistoryList';
|
import VersionsHistoryList from './HistoryList';
|
||||||
|
@ -14,16 +17,33 @@ const Versions: React.FC<Props> = ({ packageMeta, packageName }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { configOptions } = useConfig();
|
const { configOptions } = useConfig();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
if (!packageMeta) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { versions = {}, time = {}, ['dist-tags']: distTags = {} } = packageMeta;
|
const { versions = {}, time = {}, ['dist-tags']: distTags = {} } = packageMeta;
|
||||||
|
|
||||||
const hasDistTags = distTags && Object.keys(distTags).length > 0 && packageName;
|
const [packageVersions, setPackageVersions] = useState(versions);
|
||||||
const hasVersionHistory = versions && Object.keys(versions).length > 0 && packageName;
|
if (!packageMeta || Object.keys(packageMeta).length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const hideDeprecatedVersions = configOptions.hideDeprecatedVersions;
|
const hideDeprecatedVersions = configOptions.hideDeprecatedVersions;
|
||||||
|
const hasDistTags = distTags && Object.keys(distTags).length > 0 && packageName;
|
||||||
|
const hasVersionHistory =
|
||||||
|
packageVersions && Object.keys(packageVersions).length > 0 && packageName;
|
||||||
|
|
||||||
|
const filterVersions = (textSearch) => {
|
||||||
|
const filteredVersions = Object.keys(versions).reduce((acc, version) => {
|
||||||
|
if (textSearch !== '') {
|
||||||
|
if (typeof versions[version] !== 'undefined') {
|
||||||
|
if (semver.satisfies(version, textSearch, { includePrerelease: true, loose: true })) {
|
||||||
|
acc[version] = versions[version];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
acc[version] = versions[version];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
setPackageVersions(filteredVersions);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -33,11 +53,20 @@ const Versions: React.FC<Props> = ({ packageMeta, packageName }) => {
|
||||||
<VersionsTagList packageName={packageName} tags={distTags} time={time} />
|
<VersionsTagList packageName={packageName} tags={distTags} time={time} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{hasVersionHistory ? (
|
|
||||||
<>
|
<>
|
||||||
<Typography variant="subtitle1">{t('versions.version-history')}</Typography>
|
<Typography variant="subtitle1">{t('versions.version-history')}</Typography>
|
||||||
|
<TextField
|
||||||
|
helperText={t('versions.search.placeholder')}
|
||||||
|
onChange={debounce((e) => {
|
||||||
|
filterVersions(e.target.value);
|
||||||
|
}, 200)}
|
||||||
|
size="small"
|
||||||
|
variant="standard"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
{hasVersionHistory ? (
|
||||||
<>
|
<>
|
||||||
{hideDeprecatedVersions && (
|
{hideDeprecatedVersions === true && (
|
||||||
<Alert
|
<Alert
|
||||||
severity="info"
|
severity="info"
|
||||||
sx={{ marginTop: theme.spacing(1), marginBottom: theme.spacing(1) }}
|
sx={{ marginTop: theme.spacing(1), marginBottom: theme.spacing(1) }}
|
||||||
|
@ -45,8 +74,7 @@ const Versions: React.FC<Props> = ({ packageMeta, packageName }) => {
|
||||||
{t('versions.hide-deprecated')}
|
{t('versions.hide-deprecated')}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<VersionsHistoryList packageName={packageName} time={time} versions={versions} />
|
<VersionsHistoryList packageName={packageName} time={time} versions={packageVersions} />
|
||||||
</>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -14,13 +14,7 @@ interface Props {
|
||||||
const DetailContainerTabs: React.FC<Props> = ({ tabPosition, onChange }) => {
|
const DetailContainerTabs: React.FC<Props> = ({ tabPosition, onChange }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs onChange={onChange} value={tabPosition} variant={'fullWidth'}>
|
||||||
color={'primary'}
|
|
||||||
indicatorColor={'primary'}
|
|
||||||
onChange={onChange}
|
|
||||||
value={tabPosition}
|
|
||||||
variant={'fullWidth'}
|
|
||||||
>
|
|
||||||
<Tab data-testid={'readme-tab'} id={'readme-tab'} label={t('tab.readme')} />
|
<Tab data-testid={'readme-tab'} id={'readme-tab'} label={t('tab.readme')} />
|
||||||
<Tab data-testid={'dependencies-tab'} id={'dependencies-tab'} label={t('tab.dependencies')} />
|
<Tab data-testid={'dependencies-tab'} id={'dependencies-tab'} label={t('tab.dependencies')} />
|
||||||
<Tab data-testid={'versions-tab'} id={'versions-tab'} label={t('tab.versions')} />
|
<Tab data-testid={'versions-tab'} id={'versions-tab'} label={t('tab.versions')} />
|
||||||
|
|
|
@ -160,7 +160,6 @@ exports[`DetailContainer renders correctly 1`] = `
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="MuiTabs-root emotion-1 emotion-2"
|
class="MuiTabs-root emotion-1 emotion-2"
|
||||||
color="primary"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="MuiTabs-scroller MuiTabs-fixed emotion-3"
|
class="MuiTabs-scroller MuiTabs-fixed emotion-3"
|
||||||
|
|
|
@ -426,24 +426,6 @@ importers:
|
||||||
specifier: 9.9.2
|
specifier: 9.9.2
|
||||||
version: 9.9.2
|
version: 9.9.2
|
||||||
|
|
||||||
e2e/cli/e2e-pnpm6:
|
|
||||||
dependencies:
|
|
||||||
'@verdaccio/test-cli-commons':
|
|
||||||
specifier: workspace:1.1.0
|
|
||||||
version: link:../cli-commons
|
|
||||||
pnpm:
|
|
||||||
specifier: ^6.35.1
|
|
||||||
version: 6.35.1
|
|
||||||
|
|
||||||
e2e/cli/e2e-pnpm7:
|
|
||||||
dependencies:
|
|
||||||
'@verdaccio/test-cli-commons':
|
|
||||||
specifier: workspace:1.1.0
|
|
||||||
version: link:../cli-commons
|
|
||||||
pnpm:
|
|
||||||
specifier: ^7.27.1
|
|
||||||
version: 7.32.0
|
|
||||||
|
|
||||||
e2e/cli/e2e-pnpm8:
|
e2e/cli/e2e-pnpm8:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@verdaccio/test-cli-commons':
|
'@verdaccio/test-cli-commons':
|
||||||
|
@ -1950,6 +1932,9 @@ importers:
|
||||||
redux:
|
redux:
|
||||||
specifier: 4.2.1
|
specifier: 4.2.1
|
||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
|
semver:
|
||||||
|
specifier: 7.6.0
|
||||||
|
version: 7.6.0
|
||||||
validator:
|
validator:
|
||||||
specifier: 13.11.0
|
specifier: 13.11.0
|
||||||
version: 13.11.0
|
version: 13.11.0
|
||||||
|
@ -24003,18 +23988,6 @@ packages:
|
||||||
- typescript
|
- typescript
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/pnpm@6.35.1:
|
|
||||||
resolution: {integrity: sha512-Pt5JEjV2JSORAqw/XtjfF4k34PBQFpqV3kUMFYSlyrWJsio5Hr3lqy2oJkpprMrVbOyBJsxXfZDQOlvmytIrgA==}
|
|
||||||
engines: {node: '>=12.17'}
|
|
||||||
hasBin: true
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/pnpm@7.32.0:
|
|
||||||
resolution: {integrity: sha512-XkLEMinrF4046cWGvvam7dsCKeRdJ9i2SeDiKNodoZEPmJp1KrzQe1qYC5Vs/v9qBXJqyI0vLzjoMHjXgphP6g==}
|
|
||||||
engines: {node: '>=14.6'}
|
|
||||||
hasBin: true
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/pnpm@8.15.5:
|
/pnpm@8.15.5:
|
||||||
resolution: {integrity: sha512-sFGjLH5pWDO4SSbTspuMylclS1ifBknYmcbp0O22cLkex+KkNFm65zdZu1zmGcMmbxFr+THOItHvF1mn5Fqpbw==}
|
resolution: {integrity: sha512-sFGjLH5pWDO4SSbTspuMylclS1ifBknYmcbp0O22cLkex+KkNFm65zdZu1zmGcMmbxFr+THOItHvF1mn5Fqpbw==}
|
||||||
engines: {node: '>=16.14'}
|
engines: {node: '>=16.14'}
|
||||||
|
|
Loading…
Reference in a new issue