mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-16 21:56:25 -05:00
feat: complete overhaul of web user interface (#4687)
* fix: ui-component updates * Update all * Update tests * Updates * Updates * Updates * Updates * Updates * Updates * Updates * Updates * Dark logo * Add showUplinks parameter * Fix DependencyBlock links * Update * Fix highlight dark * Update * Color * Fix uncaught exception * changeset * Fix Install Settingsmenu, tsconfig * Remove duplicate function (merge issue) * Fix SideBar test and CodeQL issue
This commit is contained in:
parent
2ee28c0988
commit
10dd81f473
162 changed files with 2657 additions and 1220 deletions
10
.changeset/dirty-dolphins-try.md
Normal file
10
.changeset/dirty-dolphins-try.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
'@verdaccio/ui-components': minor
|
||||
'@verdaccio/ui-theme': patch
|
||||
'@verdaccio/types': patch
|
||||
'@verdaccio/middleware': patch
|
||||
'@verdaccio/config': patch
|
||||
'@verdaccio/cli': patch
|
||||
---
|
||||
|
||||
feat: complete overhaul of web user interface
|
|
@ -18,6 +18,12 @@ storage: ./storage
|
|||
# https://verdaccio.org/docs/webui
|
||||
web:
|
||||
title: Verdaccio
|
||||
# custom colors for header background and font
|
||||
# primaryColor: "#4b5e40"
|
||||
# custom logos and favicon
|
||||
# logo: ./path/to/logo.png
|
||||
# logoDark: ./path/to/logoDark.png
|
||||
# favicon: ./path/to/favicon.ico
|
||||
# comment out to disable gravatar support
|
||||
# gravatar: false
|
||||
# by default packages are ordercer ascendant (asc|desc)
|
||||
|
@ -35,6 +41,7 @@ web:
|
|||
# showSearch: true
|
||||
# showRaw: true
|
||||
# showDownloadTarball: true
|
||||
# showUplinks: true
|
||||
# HTML tags injected after manifest <scripts/>
|
||||
# scriptsBodyAfter:
|
||||
# - '<script type="text/javascript" src="https://my.company.com/customJS.min.js"></script>'
|
||||
|
|
|
@ -21,6 +21,12 @@ plugins: /verdaccio/plugins
|
|||
# https://verdaccio.org/docs/webui
|
||||
web:
|
||||
title: Verdaccio
|
||||
# custom colors for header background and font
|
||||
# primaryColor: "#4b5e40"
|
||||
# custom logos and favicon
|
||||
# logo: ./path/to/logo.png
|
||||
# logoDark: ./path/to/logoDark.png
|
||||
# favicon: ./path/to/favicon.ico
|
||||
# Comment out to disable gravatar support
|
||||
# gravatar: false
|
||||
# By default packages are ordered ascendant (asc|desc)
|
||||
|
@ -38,6 +44,7 @@ web:
|
|||
# showSearch: true
|
||||
# showRaw: true
|
||||
# showDownloadTarball: true
|
||||
# showUplinks: true
|
||||
# HTML tags injected after manifest <scripts/>
|
||||
# scriptsBodyAfter:
|
||||
# - '<script type="text/javascript" src="https://my.company.com/customJS.min.js"></script>'
|
||||
|
|
|
@ -83,6 +83,7 @@ export type PackageManagers = 'pnpm' | 'yarn' | 'npm';
|
|||
export type CommonWebConf = {
|
||||
title?: string;
|
||||
logo?: string;
|
||||
logoDark?: string;
|
||||
favicon?: string;
|
||||
gravatar?: boolean;
|
||||
sort_packages?: string;
|
||||
|
@ -98,6 +99,7 @@ export type CommonWebConf = {
|
|||
showFooter?: boolean;
|
||||
showThemeSwitch?: boolean;
|
||||
showDownloadTarball?: boolean;
|
||||
showUplinks?: boolean;
|
||||
hideDeprecatedVersions?: boolean;
|
||||
primaryColor: string;
|
||||
showRaw?: boolean;
|
||||
|
|
|
@ -42,34 +42,46 @@ export function renderWebMiddleware(config, tokenMiddleware, pluginOptions) {
|
|||
res.sendFile(file, sendFileCallback(next));
|
||||
});
|
||||
|
||||
// check the origin of the logo
|
||||
if (config?.web?.logo && !isURLhasValidProtocol(config?.web?.logo)) {
|
||||
// URI related to a local file
|
||||
const absoluteLocalFile = path.posix.resolve(config.web.logo);
|
||||
debug('serve local logo %s', absoluteLocalFile);
|
||||
try {
|
||||
// TODO: replace existsSync by async alternative
|
||||
if (
|
||||
fs.existsSync(absoluteLocalFile) &&
|
||||
typeof fs.accessSync(absoluteLocalFile, fs.constants.R_OK) === 'undefined'
|
||||
) {
|
||||
// Note: `path.join` will break on Windows, because it transforms `/` to `\`
|
||||
// Use POSIX version `path.posix.join` instead.
|
||||
config.web.logo = path.posix.join('/-/static/', path.basename(config.web.logo));
|
||||
router.get(config.web.logo, function (_req, res, next) {
|
||||
// @ts-ignore
|
||||
debug('serve custom logo web:%s - local:%s', config.web.logo, absoluteLocalFile);
|
||||
res.sendFile(absoluteLocalFile, sendFileCallback(next));
|
||||
});
|
||||
debug('enabled custom logo %s', config.web.logo);
|
||||
} else {
|
||||
config.web.logo = undefined;
|
||||
function renderLogo(logo: string | undefined): string | undefined {
|
||||
// check the origin of the logo
|
||||
if (logo && !isURLhasValidProtocol(logo)) {
|
||||
// URI related to a local file
|
||||
const absoluteLocalFile = path.posix.resolve(logo);
|
||||
debug('serve local logo %s', absoluteLocalFile);
|
||||
try {
|
||||
// TODO: replace existsSync by async alternative
|
||||
if (
|
||||
fs.existsSync(absoluteLocalFile) &&
|
||||
typeof fs.accessSync(absoluteLocalFile, fs.constants.R_OK) === 'undefined'
|
||||
) {
|
||||
// Note: `path.join` will break on Windows, because it transforms `/` to `\`
|
||||
// Use POSIX version `path.posix.join` instead.
|
||||
logo = path.posix.join('/-/static/', path.basename(logo));
|
||||
router.get(logo, function (_req, res, next) {
|
||||
// @ts-ignore
|
||||
debug('serve custom logo web:%s - local:%s', logo, absoluteLocalFile);
|
||||
res.sendFile(absoluteLocalFile, sendFileCallback(next));
|
||||
});
|
||||
debug('enabled custom logo %s', logo);
|
||||
} else {
|
||||
logo = undefined;
|
||||
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
|
||||
}
|
||||
} catch {
|
||||
logo = undefined;
|
||||
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
|
||||
}
|
||||
} catch {
|
||||
config.web.logo = undefined;
|
||||
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
|
||||
}
|
||||
return logo;
|
||||
}
|
||||
|
||||
const logo = renderLogo(config?.web?.logo);
|
||||
if (config?.web?.logo) {
|
||||
config.web.logo = logo;
|
||||
}
|
||||
const logoDark = renderLogo(config?.web?.logoDark);
|
||||
if (config?.web?.logoDark) {
|
||||
config.web.logoDark = logoDark;
|
||||
}
|
||||
|
||||
router.get('/-/web/:section/*', function (req, res) {
|
||||
|
|
|
@ -26,16 +26,20 @@ const defaultManifestFiles: Manifest = {
|
|||
css: [],
|
||||
};
|
||||
|
||||
export function resolveLogo(config: ConfigYaml, requestOptions: RequestOptions) {
|
||||
if (typeof config?.web?.logo !== 'string') {
|
||||
export function resolveLogo(
|
||||
logo: string | undefined,
|
||||
url_prefix: string | undefined,
|
||||
requestOptions: RequestOptions
|
||||
) {
|
||||
if (typeof logo !== 'string') {
|
||||
return '';
|
||||
}
|
||||
const isLocalFile = config?.web?.logo && !isURLhasValidProtocol(config?.web?.logo);
|
||||
const isLocalFile = logo && !isURLhasValidProtocol(logo);
|
||||
|
||||
if (isLocalFile) {
|
||||
return `${getPublicUrl(config?.url_prefix, requestOptions)}-/static/${path.basename(config?.web?.logo)}`;
|
||||
} else if (isURLhasValidProtocol(config?.web?.logo)) {
|
||||
return config?.web?.logo;
|
||||
return `${getPublicUrl(url_prefix, requestOptions)}-/static/${path.basename(logo)}`;
|
||||
} else if (isURLhasValidProtocol(logo)) {
|
||||
return logo;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
@ -61,7 +65,8 @@ export default function renderHTML(
|
|||
const title = config?.web?.title ?? WEB_TITLE;
|
||||
const login = hasLogin(config);
|
||||
const scope = config?.web?.scope ?? '';
|
||||
const logo = resolveLogo(config, requestOptions);
|
||||
const logo = resolveLogo(config?.web?.logo, config?.url_prefix, requestOptions);
|
||||
const logoDark = resolveLogo(config?.web?.logoDark, config?.url_prefix, requestOptions);
|
||||
const pkgManagers = config?.web?.pkgManagers ?? ['yarn', 'pnpm', 'npm'];
|
||||
const version = res.locals.app_version ?? '';
|
||||
const flags = {
|
||||
|
@ -81,6 +86,8 @@ export default function renderHTML(
|
|||
showFooter,
|
||||
showSearch,
|
||||
showDownloadTarball,
|
||||
showRaw,
|
||||
showUplinks,
|
||||
} = Object.assign(
|
||||
{},
|
||||
{
|
||||
|
@ -97,6 +104,8 @@ export default function renderHTML(
|
|||
showFooter,
|
||||
showSearch,
|
||||
showDownloadTarball,
|
||||
showRaw,
|
||||
showUplinks,
|
||||
darkMode,
|
||||
url_prefix,
|
||||
basename,
|
||||
|
@ -104,6 +113,7 @@ export default function renderHTML(
|
|||
primaryColor,
|
||||
version,
|
||||
logo,
|
||||
logoDark,
|
||||
flags,
|
||||
login,
|
||||
pkgManagers,
|
||||
|
|
|
@ -14,6 +14,7 @@ web:
|
|||
showRaw: true
|
||||
primary_color: '#ffffff'
|
||||
logo: './test/config/dark-logo.png'
|
||||
logoDark: './test/config/dark-logo.png'
|
||||
html_cache: false
|
||||
|
||||
url_prefix: /prefix
|
||||
|
|
|
@ -79,6 +79,13 @@ describe('test web server', () => {
|
|||
return loadLogo('file-logo.yaml', '/-/static/dark-logo.png');
|
||||
});
|
||||
|
||||
test('should render dark logo as file', async () => {
|
||||
const {
|
||||
window: { __VERDACCIO_BASENAME_UI_OPTIONS },
|
||||
} = await render('file-logo.yaml');
|
||||
expect(__VERDACCIO_BASENAME_UI_OPTIONS.logoDark).toMatch('/prefix/-/static/dark-logo.png');
|
||||
});
|
||||
|
||||
test('should not render logo as absolute file is wrong', async () => {
|
||||
const {
|
||||
window: { __VERDACCIO_BASENAME_UI_OPTIONS },
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { renderWithStore, screen } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
import { act, renderWithStore, screen } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
|
||||
import { store } from '@verdaccio/ui-components';
|
||||
|
||||
|
@ -13,17 +13,21 @@ jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600);
|
|||
/* eslint-disable react/jsx-no-bind*/
|
||||
describe('<App />', () => {
|
||||
describe('footer', () => {
|
||||
test('should display the Header component', () => {
|
||||
renderWithStore(<App />, store);
|
||||
test('should display the Header component', async () => {
|
||||
await act(() => {
|
||||
renderWithStore(<App />, store);
|
||||
});
|
||||
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not display the Header component', () => {
|
||||
test('should not display the Header component', async () => {
|
||||
// @ts-ignore
|
||||
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
|
||||
showFooter: false,
|
||||
};
|
||||
renderWithStore(<App />, store);
|
||||
await act(() => {
|
||||
renderWithStore(<App />, store);
|
||||
});
|
||||
expect(screen.queryByTestId('footer')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
"visit-home-page": "Visit homepage",
|
||||
"open-an-issue": "Open an issue",
|
||||
"download-tarball": "Download tarball",
|
||||
"raw": "Raw Manifest"
|
||||
"raw": "View manifest",
|
||||
"raw-title": "Manifest of {{package}}"
|
||||
},
|
||||
"dialog": {
|
||||
"registry-info": {
|
||||
|
@ -59,7 +60,7 @@
|
|||
"hide-deprecated": "All deprecated versions are hidden by global configuration"
|
||||
},
|
||||
"package": {
|
||||
"published-on": "Published on {{time}} •",
|
||||
"published-on": "Published {{time}}",
|
||||
"version": "v{{version}}",
|
||||
"visit-home-page": "Visit homepage",
|
||||
"homepage": "Homepage",
|
||||
|
@ -84,7 +85,7 @@
|
|||
},
|
||||
"form-validation": {
|
||||
"required-field": "This field is required",
|
||||
"required-min-length": "This field required the min length of {{length}}",
|
||||
"required-min-length": "This field required with a minimum length of {{length}}",
|
||||
"unable-to-sign-in": "Unable to sign in",
|
||||
"username-or-password-cant-be-empty": "Username or password can't be empty!"
|
||||
},
|
||||
|
@ -105,6 +106,7 @@
|
|||
},
|
||||
"installation": {
|
||||
"title": "Installation",
|
||||
"latest": "latest version",
|
||||
"global": "global package",
|
||||
"yarnModern": "yarn modern syntax"
|
||||
},
|
||||
|
@ -115,10 +117,13 @@
|
|||
"title": "Author"
|
||||
},
|
||||
"distribution": {
|
||||
"title": "Latest Distribution",
|
||||
"title": "Distribution",
|
||||
"license": "License",
|
||||
"size": "Size",
|
||||
"file-count": "file count"
|
||||
"file-count": "File Count"
|
||||
},
|
||||
"keywords": {
|
||||
"title": "Keywords"
|
||||
},
|
||||
"maintainers": {
|
||||
"title": "Maintainers"
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
"react/jsx-curly-brace-presence": ["warn", { "props": "ignore", "children": "ignore" }],
|
||||
"react/jsx-pascal-case": ["error"],
|
||||
"react/jsx-props-no-multi-spaces": ["error"],
|
||||
"react/jsx-sort-default-props": ["error"],
|
||||
"react/sort-default-props": ["error"],
|
||||
"react/jsx-sort-props": ["error"],
|
||||
"react/no-string-refs": ["error"],
|
||||
"react/no-danger-with-children": ["error"],
|
||||
|
|
3
packages/ui-components/jest/api/storybook-readme.js
Normal file
3
packages/ui-components/jest/api/storybook-readme.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = () =>
|
||||
// eslint-disable-next-line max-len
|
||||
'<h1 id="storybook-cli">Storybook CLI</h1>\n<p>This is a wrapper for <a href="https://www.npmjs.com/package/@storybook/cli">https://www.npmjs.com/package/@storybook/cli</a></p>';
|
|
@ -1,2 +0,0 @@
|
|||
<h1 id="storybook-cli">Storybook CLI</h1>
|
||||
<p>This is a wrapper for <a href="https://www.npmjs.com/package/@storybook/cli">https://www.npmjs.com/package/@storybook/cli</a></p>
|
|
@ -1,3 +1,4 @@
|
|||
import fs from 'fs';
|
||||
import { rest } from 'msw';
|
||||
|
||||
const packagesPayload = require('./api/home-packages.json');
|
||||
|
@ -21,24 +22,20 @@ export const handlers = [
|
|||
}),
|
||||
|
||||
rest.get('http://localhost:9000/-/verdaccio/data/package/readme/storybook', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.text(`<h1 id="storybook-cli">Storybook CLI MSW.js</h1>
|
||||
<p>This is a wrapper for <a href="https://www.npmjs.com/package/@storybook/cli">https://www.npmjs.com/package/@storybook/cli</a></p>
|
||||
`)
|
||||
);
|
||||
return res(ctx.text(require('./api/storybook-readme')()));
|
||||
}),
|
||||
|
||||
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/jquery', (req, res, ctx) => {
|
||||
return res(ctx.json(require('./api/jquery-sidebar.json')));
|
||||
}),
|
||||
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/JSONStream', (req, res, ctx) => {
|
||||
return res(ctx.status(401));
|
||||
return res(ctx.status(401)); // unauthorized
|
||||
}),
|
||||
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/semver', (req, res, ctx) => {
|
||||
return res(ctx.status(500));
|
||||
return res(ctx.status(500)); // internal server error
|
||||
}),
|
||||
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/kleur', (req, res, ctx) => {
|
||||
return res(ctx.status(404));
|
||||
return res(ctx.status(404)); // not found
|
||||
}),
|
||||
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/glob', (req, res, ctx) => {
|
||||
return res(ctx.json(require('./api/glob-sidebar.json')));
|
||||
|
@ -55,6 +52,14 @@ export const handlers = [
|
|||
rest.get('http://localhost:9000/-/verdaccio/data/package/readme/jquery', (req, res, ctx) => {
|
||||
return res(ctx.text(require('./api/jquery-readme')()));
|
||||
}),
|
||||
rest.get('http://localhost:9000/verdaccio/-/verdaccio-1.0.0.tgz', (req, res, ctx) => {
|
||||
const fileContent = fs.readFileSync('./api/verdaccio-1.0.0.tgz');
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/octet-stream'),
|
||||
ctx.body(fileContent)
|
||||
);
|
||||
}),
|
||||
|
||||
rest.post<{ username: string; password: string }, { token: string; username: string }>(
|
||||
'http://localhost:9000/-/verdaccio/sec/login',
|
||||
|
|
65
packages/ui-components/src/AppTest/AppRoute.test.tsx
Normal file
65
packages/ui-components/src/AppTest/AppRoute.test.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
|
||||
import { store } from '../';
|
||||
import { act, renderWithStore, screen, waitFor } from '../test/test-react-testing-library';
|
||||
import AppRoute from './AppRoute';
|
||||
|
||||
// force the windows to expand to display items
|
||||
// https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107
|
||||
jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600);
|
||||
jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600);
|
||||
|
||||
function appTest(path: string) {
|
||||
renderWithStore(
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<AppRoute />
|
||||
</MemoryRouter>,
|
||||
store
|
||||
);
|
||||
}
|
||||
|
||||
// See jest/server-handlers.ts for test routes
|
||||
describe('AppRoute', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders Front component for ROOT path', async () => {
|
||||
act(() => appTest('/'));
|
||||
await waitFor(() => expect(screen.getByTestId('loading')).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getAllByTestId('package-item-list')).toHaveLength(5));
|
||||
});
|
||||
|
||||
test('renders VersionPage component for PACKAGE path', async () => {
|
||||
act(() => appTest('/-/web/detail/jquery'));
|
||||
await waitFor(() => screen.getByTestId('readme-tab'));
|
||||
expect(screen.getByTestId('sidebar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders VersionPage component for PACKAGE VERSION path', async () => {
|
||||
act(() => appTest('/-/web/detail/jquery/v/3.6.3'));
|
||||
await waitFor(() => screen.getByTestId('readme-tab'));
|
||||
expect(screen.getByTestId('sidebar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders Forbidden component for not allowed PACKAGE', async () => {
|
||||
act(() => appTest('/-/web/detail/JSONstream'));
|
||||
await waitFor(() => screen.getByTestId('not-found-go-to-home-button'));
|
||||
});
|
||||
|
||||
test('renders NotFound component for missing PACKAGE', async () => {
|
||||
act(() => appTest('/-/web/detail/kleur'));
|
||||
await waitFor(() => screen.getByTestId('not-found-go-to-home-button'));
|
||||
});
|
||||
|
||||
test('renders NotFound component for non-existing PACKAGE VERSION', async () => {
|
||||
act(() => appTest('/-/web/detail/jquery/v/0.9.9'));
|
||||
await waitFor(() => screen.getByTestId('not-found-go-to-home-button'));
|
||||
});
|
||||
|
||||
test('renders NotFound component for non-matching path', async () => {
|
||||
act(() => appTest('/oiccadrev'));
|
||||
await waitFor(() => screen.getByTestId('not-found-go-to-home-button'));
|
||||
});
|
||||
});
|
27
packages/ui-components/src/Theme/ThemeProvider.test.tsx
Normal file
27
packages/ui-components/src/Theme/ThemeProvider.test.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
AppConfigurationProvider,
|
||||
PersistenceSettingProvider,
|
||||
StyleBaseline,
|
||||
ThemeProvider,
|
||||
} from '../';
|
||||
|
||||
const AppContainer = () => (
|
||||
<AppConfigurationProvider>
|
||||
<ThemeProvider>
|
||||
<StyleBaseline />
|
||||
<PersistenceSettingProvider>
|
||||
<div>{'Theme'}</div>
|
||||
</PersistenceSettingProvider>
|
||||
</ThemeProvider>
|
||||
</AppConfigurationProvider>
|
||||
);
|
||||
|
||||
describe('ThemeProvider', () => {
|
||||
test('should render with theme', async () => {
|
||||
render(<AppContainer />);
|
||||
await screen.findByText('Theme');
|
||||
});
|
||||
});
|
|
@ -6,11 +6,11 @@ const colors = {
|
|||
black: '#000',
|
||||
white: '#fff',
|
||||
red: '#d32f2f',
|
||||
orange: '#CD4000',
|
||||
orange: '#cd4000',
|
||||
greySuperLight: '#f5f5f5',
|
||||
greyLight: '#d3d3d3',
|
||||
greyLight2: '#908ba1',
|
||||
greyLight3: '#f3f4f240',
|
||||
greyLight3: '#f3f4f2',
|
||||
greyDark: '#a9a9a9',
|
||||
greyDark2: '#586069',
|
||||
greyChateau: '#95989a',
|
||||
|
@ -38,7 +38,7 @@ const themeModes = {
|
|||
...colors,
|
||||
primary: '#ffffff',
|
||||
secondary: '#424242',
|
||||
background: '#1A202C',
|
||||
background: '#1a202c',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,28 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
import { store } from '../../store/store';
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
renderWithStore,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '../../test/test-react-testing-library';
|
||||
import { cleanup, fireEvent, renderWithStore, screen } from '../../test/test-react-testing-library';
|
||||
import ActionBar from './ActionBar';
|
||||
|
||||
const defaultPackageMeta = {
|
||||
_uplinks: {},
|
||||
latest: {
|
||||
name: 'verdaccio-ui/local-storage',
|
||||
version: '8.0.1-next.1',
|
||||
name: 'verdaccio',
|
||||
version: '1.0.0',
|
||||
dist: {
|
||||
fileCount: 0,
|
||||
unpackedSize: 0,
|
||||
tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
|
||||
fileCount: 1,
|
||||
unpackedSize: 171,
|
||||
tarball: 'http://localhost:9000/verdaccio/-/verdaccio-1.0.0.tgz',
|
||||
},
|
||||
homepage: 'https://verdaccio.org',
|
||||
bugs: {
|
||||
url: 'https://github.com/verdaccio/monorepo/issues',
|
||||
url: 'https://github.com/verdaccio/verdaccio/issues',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -39,6 +33,12 @@ describe('<ActionBar /> component', () => {
|
|||
expect(screen.getByTestId('HomeIcon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render if data is missing', () => {
|
||||
// @ts-ignore - testing with missing data
|
||||
renderWithStore(<ActionBar packageMeta={undefined} />, store);
|
||||
expect(screen.queryByTestId('HomeIcon')).toBeNull();
|
||||
});
|
||||
|
||||
test('when there is no action bar data', () => {
|
||||
const packageMeta = {
|
||||
...defaultPackageMeta,
|
||||
|
@ -64,6 +64,14 @@ describe('<ActionBar /> component', () => {
|
|||
expect(screen.getByLabelText('action-bar-action.download-tarball')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('when button to download is disabled', () => {
|
||||
renderWithStore(
|
||||
<ActionBar packageMeta={defaultPackageMeta} showDownloadTarball={false} />,
|
||||
store
|
||||
);
|
||||
expect(screen.queryByTestId('download-tarball-btn')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when there is a button to raw manifest', () => {
|
||||
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
|
||||
expect(screen.getByLabelText('action-bar-action.raw')).toBeTruthy();
|
||||
|
@ -71,15 +79,28 @@ describe('<ActionBar /> component', () => {
|
|||
|
||||
test('when click button to raw manifest open a dialog with viewer', async () => {
|
||||
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
|
||||
expect(screen.queryByTestId('rawViewer--dialog')).toBeFalsy();
|
||||
|
||||
fireEvent.click(screen.getByLabelText('action-bar-action.raw'));
|
||||
await waitFor(() => expect(screen.getByTestId('rawViewer--dialog')).toBeInTheDocument());
|
||||
await screen.findByTestId('rawViewer--dialog');
|
||||
|
||||
fireEvent.click(screen.getByTestId('close-raw-viewer'));
|
||||
await screen.getByLabelText('action-bar-action.raw');
|
||||
|
||||
expect(screen.queryByTestId('rawViewer--dialog')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should not display download tarball button', () => {
|
||||
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
|
||||
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={false} />, store);
|
||||
expect(screen.queryByLabelText('Download tarball')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('when click button to download ', async () => {
|
||||
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={false} />, store);
|
||||
fireEvent.click(screen.getByTestId('download-tarball-btn'));
|
||||
await store.getState().loading.models.download;
|
||||
});
|
||||
|
||||
test('should not display show raw button', () => {
|
||||
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={false} />, store);
|
||||
expect(screen.queryByLabelText('action-bar-action.raw')).toBeFalsy();
|
||||
|
|
|
@ -41,7 +41,7 @@ const ActionBar: React.FC<Props> = ({ showRaw, showDownloadTarball = true, packa
|
|||
}
|
||||
|
||||
return (
|
||||
<Box alignItems="center" display="flex" marginBottom="14px">
|
||||
<Box alignItems="center" display="flex" sx={{ my: 2 }}>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{actions.map((action) => (
|
||||
<ActionBarAction key={action.type} {...action} />
|
||||
|
|
|
@ -12,11 +12,14 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||
|
||||
import { Theme } from '../../Theme';
|
||||
import { Dispatch, RootState } from '../../store/store';
|
||||
import { Link } from '../Link';
|
||||
import LinkExternal from '../LinkExternal';
|
||||
|
||||
export const Fab = styled(FabMUI)<{ theme?: Theme }>(({ theme }) => ({
|
||||
backgroundColor:
|
||||
theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue,
|
||||
'&:hover': {
|
||||
color: theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue,
|
||||
},
|
||||
color: theme?.palette.white,
|
||||
}));
|
||||
|
||||
|
@ -42,21 +45,21 @@ const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link, action })
|
|||
case 'VISIT_HOMEPAGE':
|
||||
return (
|
||||
<Tooltip title={t('action-bar-action.visit-home-page') as string}>
|
||||
<Link external={true} to={link} variant="button">
|
||||
<LinkExternal to={link} variant="button">
|
||||
<Fab size="small">
|
||||
<HomeIcon />
|
||||
</Fab>
|
||||
</Link>
|
||||
</LinkExternal>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'OPEN_AN_ISSUE':
|
||||
return (
|
||||
<Tooltip title={t('action-bar-action.open-an-issue') as string}>
|
||||
<Link external={true} to={link} variant="button">
|
||||
<LinkExternal to={link} variant="button">
|
||||
<Fab size="small">
|
||||
<BugReportIcon />
|
||||
</Fab>
|
||||
</Link>
|
||||
</LinkExternal>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'DOWNLOAD_TARBALL':
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { cleanup, render } from '../../test/test-react-testing-library';
|
||||
import { cleanup, render, screen } from '../../test/test-react-testing-library';
|
||||
import { PackageMetaInterface } from '../../types/packageMeta';
|
||||
import Authors from './Author';
|
||||
|
||||
|
@ -66,4 +66,10 @@ describe('<Author /> component', () => {
|
|||
const wrapper = render(withAuthorComponent(packageMeta));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not render if data is missing', () => {
|
||||
// @ts-ignore - testing with missing data
|
||||
render(withAuthorComponent(undefined));
|
||||
expect(screen.queryByText('sidebar.author.title')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,35 +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 Person from '../Person';
|
||||
import { AuthorListItem, StyledText } from './styles';
|
||||
|
||||
export function getAuthorName(authorName?: string): string {
|
||||
if (!authorName) {
|
||||
return i18next.t('author-unknown');
|
||||
}
|
||||
|
||||
if (authorName.toLowerCase() === 'anonymous') {
|
||||
return i18next.t('author-anonymous');
|
||||
}
|
||||
|
||||
return authorName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
const Author: FC<{ packageMeta }> = ({ packageMeta }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
if (!packageMeta) {
|
||||
return null;
|
||||
}
|
||||
|
@ -40,26 +18,10 @@ const Author: FC<{ packageMeta }> = ({ packageMeta }) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { email, name } = author;
|
||||
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>}>
|
||||
<AuthorListItem>
|
||||
{!email || !url.isEmail(email) ? (
|
||||
avatarComponent
|
||||
) : (
|
||||
<a href={`mailto:${email}?subject=${packageName}@${version}`} target={'_top'}>
|
||||
{avatarComponent}
|
||||
</a>
|
||||
)}
|
||||
{name && <Typography variant="subtitle2">{getAuthorName(name)}</Typography>}
|
||||
<AuthorListItem sx={{ my: 1 }}>
|
||||
<Person packageName={packageName} person={author} version={version} withText={true} />
|
||||
</AuthorListItem>
|
||||
</List>
|
||||
);
|
||||
|
|
|
@ -44,6 +44,8 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
padding-bottom: 8px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -68,6 +70,22 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
}
|
||||
|
||||
.emotion-5 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.66;
|
||||
color: #4b5e40;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-5:hover {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emotion-6 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
|
@ -97,10 +115,11 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
user-select: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.emotion-6 {
|
||||
.emotion-7 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
@ -109,12 +128,13 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
text-indent: 10000px;
|
||||
}
|
||||
|
||||
.emotion-7 {
|
||||
.emotion-8 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.57;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
<body>
|
||||
|
@ -131,21 +151,25 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
class="MuiListItem-root MuiListItem-gutters MuiListItem-padding emotion-3 emotion-4"
|
||||
>
|
||||
<a
|
||||
href="mailto:verdaccio.user@verdaccio.org?subject=verdaccio@4.0.0"
|
||||
target="_top"
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-5"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="verdaccio user"
|
||||
href="mailto:verdaccio.user@verdaccio.org?subject=verdaccio v4.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
||||
class="MuiAvatar-root MuiAvatar-circular emotion-6"
|
||||
>
|
||||
<img
|
||||
alt="verdaccio user"
|
||||
class="MuiAvatar-img emotion-6"
|
||||
class="MuiAvatar-img emotion-7"
|
||||
src="https://www.gravatar.com/avatar/000000"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<h6
|
||||
class="MuiTypography-root MuiTypography-subtitle2 emotion-7"
|
||||
class="MuiTypography-root MuiTypography-subtitle2 emotion-8"
|
||||
>
|
||||
verdaccio user
|
||||
</h6>
|
||||
|
@ -194,6 +218,8 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
padding-bottom: 8px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -218,6 +244,22 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
}
|
||||
|
||||
.emotion-5 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.66;
|
||||
color: #4b5e40;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-5:hover {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emotion-6 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
|
@ -247,10 +289,11 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
user-select: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.emotion-6 {
|
||||
.emotion-7 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
@ -259,12 +302,13 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
text-indent: 10000px;
|
||||
}
|
||||
|
||||
.emotion-7 {
|
||||
.emotion-8 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.57;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
<div>
|
||||
|
@ -280,21 +324,25 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||
class="MuiListItem-root MuiListItem-gutters MuiListItem-padding emotion-3 emotion-4"
|
||||
>
|
||||
<a
|
||||
href="mailto:verdaccio.user@verdaccio.org?subject=verdaccio@4.0.0"
|
||||
target="_top"
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-5"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="verdaccio user"
|
||||
href="mailto:verdaccio.user@verdaccio.org?subject=verdaccio v4.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
||||
class="MuiAvatar-root MuiAvatar-circular emotion-6"
|
||||
>
|
||||
<img
|
||||
alt="verdaccio user"
|
||||
class="MuiAvatar-img emotion-6"
|
||||
class="MuiAvatar-img emotion-7"
|
||||
src="https://www.gravatar.com/avatar/000000"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<h6
|
||||
class="MuiTypography-root MuiTypography-subtitle2 emotion-7"
|
||||
class="MuiTypography-root MuiTypography-subtitle2 emotion-8"
|
||||
>
|
||||
verdaccio user
|
||||
</h6>
|
||||
|
@ -399,6 +447,8 @@ exports[`<Author /> component should render the component when there is no autho
|
|||
padding-bottom: 8px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -452,6 +502,7 @@ exports[`<Author /> component should render the component when there is no autho
|
|||
user-select: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
@ -470,6 +521,7 @@ exports[`<Author /> component should render the component when there is no autho
|
|||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.57;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
<body>
|
||||
|
@ -487,6 +539,8 @@ exports[`<Author /> component should render the component when there is no autho
|
|||
>
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="verdaccio user"
|
||||
>
|
||||
<img
|
||||
alt="verdaccio user"
|
||||
|
@ -544,6 +598,8 @@ exports[`<Author /> component should render the component when there is no autho
|
|||
padding-bottom: 8px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -597,6 +653,7 @@ exports[`<Author /> component should render the component when there is no autho
|
|||
user-select: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
@ -615,6 +672,7 @@ exports[`<Author /> component should render the component when there is no autho
|
|||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.57;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
<div>
|
||||
|
@ -631,6 +689,8 @@ exports[`<Author /> component should render the component when there is no autho
|
|||
>
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="verdaccio user"
|
||||
>
|
||||
<img
|
||||
alt="verdaccio user"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
|
||||
import { fireEvent, render, screen } from '../../test/test-react-testing-library';
|
||||
import CopyToClipBoard from './CopyToClipBoard';
|
||||
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: jest.fn().mockImplementation(() => Promise.resolve()) },
|
||||
});
|
||||
|
||||
describe('CopyToClipBoard component', () => {
|
||||
test('should copy text to clipboard', async () => {
|
||||
const copyThis = 'copy this';
|
||||
render(
|
||||
<CopyToClipBoard dataTestId={'copy-component'} text={copyThis} title={`npm i verdaccio`} />
|
||||
);
|
||||
expect(screen.getByTestId('copy-component')).toBeInTheDocument();
|
||||
|
||||
const copyComponent = await screen.findByTestId('copy-component');
|
||||
await fireEvent.click(copyComponent);
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyThis);
|
||||
});
|
||||
});
|
|
@ -5,17 +5,6 @@ export const copyToClipBoardUtility =
|
|||
(event: SyntheticEvent<HTMLElement>): void => {
|
||||
event.preventDefault();
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.innerText = str;
|
||||
if (document.body) {
|
||||
document.body.appendChild(node);
|
||||
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection() as Selection;
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(node);
|
||||
}
|
||||
// document.execCommand is deprecated
|
||||
navigator.clipboard.writeText(str);
|
||||
};
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
import styled from '@emotion/styled';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Theme } from '../../Theme';
|
||||
import NoItems from '../NoItems';
|
||||
import { DependencyBlock } from './DependencyBlock';
|
||||
import { hasKeys } from './utits';
|
||||
|
||||
export const CardWrap = styled(Card)<{ theme?: Theme }>((props) => ({
|
||||
marginBottom: props.theme.spacing(2),
|
||||
}));
|
||||
|
||||
const Dependencies: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
@ -43,7 +37,7 @@ const Dependencies: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
|||
hasKeys(peerDependencies);
|
||||
if (hasDependencies) {
|
||||
return (
|
||||
<CardWrap>
|
||||
<Card sx={{ mb: 2 }}>
|
||||
<CardContent>
|
||||
<Box data-testid="dependencies-box" sx={{ m: 2 }}>
|
||||
{Object.entries(dependencyMap).map(([dependencyType, dependencies]) => {
|
||||
|
@ -62,11 +56,17 @@ const Dependencies: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
|||
})}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</CardWrap>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return <NoItems text={t('dependencies.has-no-dependencies', { package: name })} />;
|
||||
return (
|
||||
<Card sx={{ mb: 2 }}>
|
||||
<CardContent>
|
||||
<NoItems text={t('dependencies.has-no-dependencies', { package: name })} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dependencies;
|
||||
|
|
|
@ -9,6 +9,7 @@ import { useHistory } from 'react-router-dom';
|
|||
|
||||
import { Theme } from '../../Theme';
|
||||
import { PackageDependencies } from '../../types/packageMeta';
|
||||
import { Route } from '../../utils';
|
||||
|
||||
interface DependencyBlockProps {
|
||||
title: string;
|
||||
|
@ -41,7 +42,7 @@ export const DependencyBlock: React.FC<DependencyBlockProps> = ({ title, depende
|
|||
const deps = Object.entries(dependencies);
|
||||
|
||||
function handleClick(name: string): void {
|
||||
history.push(`/-/web/detail/${name}`);
|
||||
history.push(`${Route.DETAIL}${name}`);
|
||||
}
|
||||
|
||||
function labelText(title: string, name: string, version: string): string {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export enum DeveloperType {
|
||||
CONTRIBUTORS = 'contributors',
|
||||
MAINTAINERS = 'maintainers',
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import { cleanup, fireEvent, render } from '../../test/test-react-testing-library';
|
||||
import { cleanup, fireEvent, render, screen } from '../../test/test-react-testing-library';
|
||||
import { DeveloperType } from './DeveloperType';
|
||||
import Developers from './Developers';
|
||||
import { DeveloperType } from './Title';
|
||||
|
||||
describe('test Developers', () => {
|
||||
afterEach(() => {
|
||||
|
@ -63,7 +63,7 @@ describe('test Developers', () => {
|
|||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should test onClick the component avatar', () => {
|
||||
test('should show only up to max items', () => {
|
||||
const packageMeta = {
|
||||
latest: {
|
||||
packageName: 'foo',
|
||||
|
@ -72,6 +72,7 @@ describe('test Developers', () => {
|
|||
{
|
||||
name: 'dmethvin',
|
||||
email: 'test@gmail.com',
|
||||
url: 'https://example.com/',
|
||||
},
|
||||
{
|
||||
name: 'dmethvin2',
|
||||
|
@ -89,14 +90,40 @@ describe('test Developers', () => {
|
|||
<Developers packageMeta={packageMeta} type={DeveloperType.CONTRIBUTORS} visibleMax={1} />
|
||||
);
|
||||
|
||||
// const item2 = wrapper.find(Fab);
|
||||
// // TODO: I am not sure here how to verify the method inside the component was called.
|
||||
// item2.simulate('click');
|
||||
|
||||
expect(wrapper.getByText('sidebar.contributors.title')).toBeInTheDocument();
|
||||
fireEvent.click(wrapper.getByRole('button'));
|
||||
expect(wrapper.getByTestId(packageMeta.latest.contributors[0].name)).toBeInTheDocument();
|
||||
expect(wrapper.queryByTestId(packageMeta.latest.contributors[1].name)).not.toBeInTheDocument();
|
||||
expect(wrapper.queryByTestId(packageMeta.latest.contributors[2].name)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(wrapper.getByLabelText(packageMeta.latest.contributors[0].name)).toBeInTheDocument();
|
||||
expect(wrapper.getByLabelText(packageMeta.latest.contributors[1].name)).toBeInTheDocument();
|
||||
test('renders only the first six contributors when there are more than six', () => {
|
||||
const packageMeta = {
|
||||
latest: {
|
||||
contributors: [
|
||||
{ name: 'contributor1', email: 'c1@test.com' },
|
||||
{ name: 'contributor2', email: 'c2@test.com' },
|
||||
{ name: 'contributor3', email: 'c3@test.com' },
|
||||
{ name: 'contributor4', email: 'c4@test.com' },
|
||||
{ name: 'contributor5', email: 'c5@test.com' },
|
||||
{ name: 'contributor6', email: 'c6@test.com' },
|
||||
{ name: 'contributor7', email: 'c7@test.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
render(<Developers packageMeta={packageMeta} type={DeveloperType.CONTRIBUTORS} />);
|
||||
|
||||
expect(screen.getByTestId('contributor1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('contributor2')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('contributor3')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('contributor4')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('contributor5')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('contributor6')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('contributor7')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('fab-add')).toBeInTheDocument();
|
||||
|
||||
// click on "more"
|
||||
fireEvent.click(screen.getByTestId('fab-add'));
|
||||
expect(screen.getByTestId('contributor7')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
import styled from '@emotion/styled';
|
||||
import Add from '@mui/icons-material/Add';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import FabMUI from '@mui/material/Fab';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Theme } from '../../Theme';
|
||||
import Person from '../Person';
|
||||
import { DeveloperType } from './DeveloperType';
|
||||
import Title from './Title';
|
||||
import getUniqueDeveloperValues from './get-unique-developer-values';
|
||||
|
||||
export enum DeveloperType {
|
||||
CONTRIBUTORS = 'contributors',
|
||||
MAINTAINERS = 'maintainers',
|
||||
}
|
||||
|
||||
export const Fab = styled(FabMUI)<{ theme?: Theme }>((props) => ({
|
||||
backgroundColor: props.theme?.palette.primary.main,
|
||||
color: props.theme?.palette.white,
|
||||
|
@ -28,7 +23,7 @@ interface Props {
|
|||
|
||||
const StyledBox = styled(Box)({
|
||||
'> *': {
|
||||
margin: 5,
|
||||
marginRight: 5,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -58,19 +53,24 @@ const Developers: React.FC<Props> = ({ type, visibleMax = VISIBLE_MAX, packageMe
|
|||
return null;
|
||||
}
|
||||
|
||||
const { name: packageName, version } = packageMeta.latest;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title type={type} />
|
||||
<StyledBox display="flex" flexWrap="wrap" margin="10px 0 10px 0">
|
||||
{visibleDevelopers.map((visibleDeveloper) => {
|
||||
{visibleDevelopers.map((visibleDeveloper, index) => {
|
||||
return (
|
||||
<Tooltip key={visibleDeveloper.email} title={visibleDeveloper.name}>
|
||||
<Avatar alt={visibleDeveloper.name} src={visibleDeveloper.avatar} />
|
||||
</Tooltip>
|
||||
<Person
|
||||
key={index}
|
||||
packageName={packageName}
|
||||
person={visibleDeveloper}
|
||||
version={version}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{visibleDevelopersMax < developers.length && (
|
||||
<Fab onClick={handleSetVisibleDevelopersMax} size="small">
|
||||
<Fab data-testid={'fab-add'} onClick={handleSetVisibleDevelopersMax} size="small">
|
||||
<Add />
|
||||
</Fab>
|
||||
)}
|
||||
|
|
|
@ -4,11 +4,7 @@ import React from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Theme } from '../../Theme';
|
||||
|
||||
export enum DeveloperType {
|
||||
CONTRIBUTORS = 'contributors',
|
||||
MAINTAINERS = 'maintainers',
|
||||
}
|
||||
import { DeveloperType } from './';
|
||||
|
||||
interface Props {
|
||||
type: DeveloperType;
|
||||
|
|
|
@ -26,10 +26,26 @@ exports[`test Developers should render the component for contributors with items
|
|||
}
|
||||
|
||||
.emotion-3>* {
|
||||
margin: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.emotion-4 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.66;
|
||||
color: #4b5e40;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-4:hover {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emotion-5 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
|
@ -59,9 +75,13 @@ exports[`test Developers should render the component for contributors with items
|
|||
user-select: none;
|
||||
color: #f4f4f4;
|
||||
background-color: #bdbdbd;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.emotion-5 {
|
||||
.emotion-6 {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
|
@ -90,40 +110,54 @@ exports[`test Developers should render the component for contributors with items
|
|||
<div
|
||||
class="emotion-2 MuiBox-root emotion-3"
|
||||
>
|
||||
<div
|
||||
aria-label="dmethvin"
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="dmethvin"
|
||||
href="mailto:test@gmail.com?subject=undefined v1.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="mgol"
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="mgol"
|
||||
href="mailto:m.goleb@gmail.com?subject=undefined v1.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
|
@ -150,10 +184,26 @@ exports[`test Developers should render the component for contributors with items
|
|||
}
|
||||
|
||||
.emotion-3>* {
|
||||
margin: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.emotion-4 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.66;
|
||||
color: #4b5e40;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-4:hover {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emotion-5 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
|
@ -183,9 +233,13 @@ exports[`test Developers should render the component for contributors with items
|
|||
user-select: none;
|
||||
color: #f4f4f4;
|
||||
background-color: #bdbdbd;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.emotion-5 {
|
||||
.emotion-6 {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
|
@ -213,40 +267,54 @@ exports[`test Developers should render the component for contributors with items
|
|||
<div
|
||||
class="emotion-2 MuiBox-root emotion-3"
|
||||
>
|
||||
<div
|
||||
aria-label="dmethvin"
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="dmethvin"
|
||||
href="mailto:test@gmail.com?subject=undefined v1.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="mgol"
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="mgol"
|
||||
href="mailto:m.goleb@gmail.com?subject=undefined v1.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
|
@ -329,10 +397,26 @@ exports[`test Developers should render the component for maintainers with items
|
|||
}
|
||||
|
||||
.emotion-3>* {
|
||||
margin: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.emotion-4 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.66;
|
||||
color: #4b5e40;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-4:hover {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emotion-5 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
|
@ -362,9 +446,13 @@ exports[`test Developers should render the component for maintainers with items
|
|||
user-select: none;
|
||||
color: #f4f4f4;
|
||||
background-color: #bdbdbd;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.emotion-5 {
|
||||
.emotion-6 {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
|
@ -393,40 +481,54 @@ exports[`test Developers should render the component for maintainers with items
|
|||
<div
|
||||
class="emotion-2 MuiBox-root emotion-3"
|
||||
>
|
||||
<div
|
||||
aria-label="dmethvin"
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="dmethvin"
|
||||
href="mailto:test@gmail.com?subject=undefined v1.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="mgol"
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="mgol"
|
||||
href="mailto:m.goleb@gmail.com?subject=undefined v1.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
|
@ -453,10 +555,26 @@ exports[`test Developers should render the component for maintainers with items
|
|||
}
|
||||
|
||||
.emotion-3>* {
|
||||
margin: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.emotion-4 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.66;
|
||||
color: #4b5e40;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-4:hover {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emotion-5 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
|
@ -486,9 +604,13 @@ exports[`test Developers should render the component for maintainers with items
|
|||
user-select: none;
|
||||
color: #f4f4f4;
|
||||
background-color: #bdbdbd;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.emotion-5 {
|
||||
.emotion-6 {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
|
@ -516,40 +638,54 @@ exports[`test Developers should render the component for maintainers with items
|
|||
<div
|
||||
class="emotion-2 MuiBox-root emotion-3"
|
||||
>
|
||||
<div
|
||||
aria-label="dmethvin"
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="dmethvin"
|
||||
href="mailto:test@gmail.com?subject=undefined v1.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
aria-label="mgol"
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||
data-mui-internal-clone-element="true"
|
||||
data-testid="mgol"
|
||||
href="mailto:m.goleb@gmail.com?subject=undefined v1.0.0"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||
data-testid="PersonIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export { default, DeveloperType } from './Developers';
|
||||
export { default } from './Developers';
|
||||
export { DeveloperType } from './DeveloperType';
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
import FabMUI from '@mui/material/Fab';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import { Theme } from '../../Theme';
|
||||
|
||||
export const Details = styled('span')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const Content = styled('div')({
|
||||
margin: '10px 0 10px 0',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
},
|
||||
});
|
||||
|
||||
export const StyledText = styled(Typography)<{ theme?: Theme }>((props) => ({
|
||||
fontWeight: props.theme?.fontWeight.bold,
|
||||
marginBottom: '10px',
|
||||
textTransform: 'capitalize',
|
||||
}));
|
||||
|
||||
export const Fab = styled(FabMUI)<{ theme?: Theme }>((props) => ({
|
||||
backgroundColor: props.theme?.palette.primary.main,
|
||||
color: props.theme?.palette.white,
|
||||
}));
|
|
@ -33,7 +33,7 @@ describe('<Dist /> component', () => {
|
|||
expect(getByText('sidebar.distribution.size')).toBeInTheDocument();
|
||||
expect(getByText('sidebar.distribution.size')).toBeInTheDocument();
|
||||
expect(getByText('7', { exact: false })).toBeInTheDocument();
|
||||
expect(getByText('10.00 Bytes', { exact: false })).toBeInTheDocument();
|
||||
expect(getByText('10 Bytes', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the component with license as string', () => {
|
||||
|
|
|
@ -3,8 +3,8 @@ import React, { FC } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PackageMetaInterface } from '../../types/packageMeta';
|
||||
import { fileSizeSI, formatLicense } from '../../utils/utils';
|
||||
import { DistChips, DistListItem, StyledText } from './styles';
|
||||
import { fileSizeSI, formatLicense } from './utils';
|
||||
|
||||
const DistChip: FC<{ name: string; children?: React.ReactElement | string }> = ({
|
||||
name,
|
||||
|
@ -47,9 +47,7 @@ const Dist: FC<{ packageMeta: PackageMetaInterface }> = ({ packageMeta }) => {
|
|||
<DistChip name={t('sidebar.distribution.size')}>{fileSizeSI(dist.unpackedSize)}</DistChip>
|
||||
) : null}
|
||||
|
||||
<DistChip name={t('sidebar.distribution.license')}>
|
||||
{formatLicense(license as string)}
|
||||
</DistChip>
|
||||
<DistChip name={t('sidebar.distribution.license')}>{formatLicense(license)}</DistChip>
|
||||
</DistListItem>
|
||||
</List>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import styled from '@emotion/styled';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import FabMUI from '@mui/material/Fab';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
|
@ -22,8 +21,3 @@ export const DistChips = styled(Chip)({
|
|||
textTransform: 'capitalize',
|
||||
marginTop: 5,
|
||||
});
|
||||
|
||||
export const DownloadButton = styled(FabMUI)<{ theme?: Theme }>((props) => ({
|
||||
backgroundColor: props.theme?.palette.primary.main,
|
||||
color: props.theme?.palette.white,
|
||||
}));
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import { LicenseInterface } from '../../types/packageMeta';
|
||||
|
||||
/**
|
||||
* Formats license field for webui.
|
||||
* @see https://docs.npmjs.com/files/package.json#license
|
||||
*/
|
||||
// License should use type License defined above, but conflicts with the unit test that provide array or empty object
|
||||
export function formatLicense(license: string | LicenseInterface): string | undefined {
|
||||
if (typeof license === 'string') {
|
||||
return license;
|
||||
}
|
||||
|
||||
if (license?.type) {
|
||||
return license.type;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export function fileSizeSI(
|
||||
a: number,
|
||||
b?: typeof Math,
|
||||
c?: (p: number) => number,
|
||||
d?: number,
|
||||
e?: number
|
||||
): string {
|
||||
return (
|
||||
((b = Math), (c = b.log), (d = 1e3), (e = (c(a) / c(d)) | 0), a / b.pow(d, e)).toFixed(2) +
|
||||
' ' +
|
||||
(e ? 'kMGTPEZY'[--e] + 'B' : 'Bytes')
|
||||
);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { fileSizeSI, formatLicense } from './utils';
|
||||
|
||||
test('formatLicense as string', () => {
|
||||
expect(formatLicense('MIT')).toEqual('MIT');
|
||||
});
|
||||
|
||||
test('formatLicense as format object', () => {
|
||||
expect(formatLicense({ type: 'MIT' })).toEqual('MIT');
|
||||
});
|
||||
|
||||
test('fileSizeSI as number 1000', () => {
|
||||
expect(fileSizeSI(1000)).toEqual('1.00 kB');
|
||||
});
|
||||
|
||||
test('fileSizeSI as number 0', () => {
|
||||
expect(fileSizeSI(0)).toEqual('0.00 Bytes');
|
||||
});
|
|
@ -23,8 +23,12 @@ const EngineItem: FC<EngineItemProps> = ({ title, element, engineText }) => (
|
|||
<Grid item={true} xs={6}>
|
||||
<List subheader={<StyledText variant={'subtitle1'}>{title}</StyledText>}>
|
||||
<EngineListItem>
|
||||
<Avatar sx={{ bgcolor: 'transparent' }}>{element}</Avatar>
|
||||
<Typography variant="subtitle2">{engineText}</Typography>
|
||||
<Avatar sx={{ backgroundColor: 'transparent', marginLeft: 0, padding: 0 }}>
|
||||
{element}
|
||||
</Avatar>
|
||||
<Typography sx={{ margin: 0, padding: '0 0 0 10px' }} variant="subtitle2">
|
||||
{engineText}
|
||||
</Typography>
|
||||
</EngineListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
|
|
|
@ -10,5 +10,5 @@ export const StyledText = styled(Typography)<{ theme?: Theme }>((props) => ({
|
|||
}));
|
||||
|
||||
export const EngineListItem = styled(ListItem)({
|
||||
paddingLeft: 0,
|
||||
padding: 0,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../test/test-react-testing-library';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
|
||||
describe('ErrorBoundary component', () => {
|
||||
test('should render children when no error is caught', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<div>{'Test'}</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render error information when error is caught', () => {
|
||||
const ErrorComponent = () => {
|
||||
throw new Error('Test error');
|
||||
};
|
||||
|
||||
// Suppress error messages for this test
|
||||
const spy = jest.spyOn(console, 'error');
|
||||
spy.mockImplementation(() => {});
|
||||
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ErrorComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Something went wrong.')).toBeInTheDocument();
|
||||
expect(screen.getByText(/error:/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/info:/)).toBeInTheDocument();
|
||||
|
||||
// Restore console.error after test
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
|
@ -6,11 +6,11 @@ import { Trans } from 'react-i18next';
|
|||
|
||||
import { Theme } from '../../Theme';
|
||||
import { url } from '../../utils';
|
||||
import { Link } from '../Link';
|
||||
import LinkExternal from '../LinkExternal';
|
||||
|
||||
const StyledLink = styled(Link)<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginTop: theme?.spacing(1),
|
||||
marginBottom: theme?.spacing(1),
|
||||
const StyledLink = styled(LinkExternal)<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginTop: theme?.spacing(2),
|
||||
marginBottom: theme?.spacing(2),
|
||||
textDecoration: 'none',
|
||||
display: 'block',
|
||||
}));
|
||||
|
@ -32,7 +32,7 @@ const FundButton: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<StyledLink external={true} to={fundingUrl} variant="button">
|
||||
<StyledLink to={fundingUrl} variant="button">
|
||||
<Button
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
|
||||
import { fireEvent, render, screen } from '../../test/test-react-testing-library';
|
||||
import HeaderInfoDialog from './HeaderInfoDialog';
|
||||
|
||||
describe('HeaderInfoDialog', () => {
|
||||
const onCloseDialog = jest.fn();
|
||||
|
||||
const tabs = [{ label: 'Tab 1' }, { label: 'Tab 2' }];
|
||||
|
||||
const tabPanels = [{ element: <div>{'Panel 1'}</div> }, { element: <div>{'Panel 2'}</div> }];
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<HeaderInfoDialog
|
||||
dialogTitle="Dialog Title"
|
||||
isOpen={true}
|
||||
onCloseDialog={onCloseDialog}
|
||||
tabPanels={tabPanels}
|
||||
tabs={tabs}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('renders without crashing', () => {
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays the dialog title', () => {
|
||||
expect(screen.getByText('Dialog Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the tabs correctly', () => {
|
||||
expect(screen.getByText('Tab 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tab 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the tab panels correctly', async () => {
|
||||
expect(screen.getByText('Panel 1')).toBeInTheDocument();
|
||||
// Panel 2 should not be visible initially
|
||||
expect(screen.queryByText('Panel 2')).not.toBeInTheDocument();
|
||||
// Switch to Tab 2
|
||||
fireEvent.click(screen.getByText('Tab 2'));
|
||||
await expect(screen.queryByText('Panel 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onCloseDialog when the dialog is closed', () => {
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(onCloseDialog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -54,7 +54,7 @@ const HeaderInfoDialog: React.FC<Props> = ({
|
|||
<RegistryInfoDialog onClose={onCloseDialog} open={isOpen} title={dialogTitle}>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs aria-label="infoTabs" onChange={handleChange} value={value}>
|
||||
<Tabs aria-label="infoTabs" data-testid={'tabs'} onChange={handleChange} value={value}>
|
||||
{tabs
|
||||
? tabs.map((item, index) => {
|
||||
return (
|
||||
|
@ -68,7 +68,7 @@ const HeaderInfoDialog: React.FC<Props> = ({
|
|||
{tabPanels
|
||||
? tabPanels.map((item, index) => {
|
||||
return (
|
||||
<TabPanel index={index} key={item.key} value={value}>
|
||||
<TabPanel index={index} key={index} value={value}>
|
||||
{item.element}
|
||||
</TabPanel>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../test/test-react-testing-library';
|
||||
import Heading from './Heading';
|
||||
|
||||
describe('Heading component', () => {
|
||||
test('should render correctly with default props', () => {
|
||||
render(<Heading>{'Test'}</Heading>);
|
||||
const headingElement = screen.getByText('Test');
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
expect(headingElement.tagName).toBe('H6');
|
||||
});
|
||||
|
||||
test('should render correctly with custom props', () => {
|
||||
render(<Heading variant="h1">{'Test'}</Heading>);
|
||||
const headingElement = screen.getByText('Test');
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
expect(headingElement.tagName).toBe('H1');
|
||||
});
|
||||
});
|
|
@ -1,13 +1,7 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const icon = require('./commonjs.svg');
|
||||
|
||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginLeft: theme?.spacing(1),
|
||||
}));
|
||||
|
||||
export function CommonJS() {
|
||||
return <ImgIcon alt="commonjs" height="20" src={icon} width="20" />;
|
||||
return <img alt="commonjs" height="20" src={icon} width="20" />;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const icon = require('./es6modules.svg');
|
||||
|
||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginLeft: theme?.spacing(1),
|
||||
}));
|
||||
const icon = require('./es6module.svg');
|
||||
|
||||
export function ES6Modules() {
|
||||
return <ImgIcon alt="es6 modules" height="20" src={icon} width="20" />;
|
||||
return <img alt="es6 modules" height="20" src={icon} width="20" />;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const icon = require('./git.svg');
|
||||
|
||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginLeft: theme?.spacing(1),
|
||||
}));
|
||||
|
||||
export function Git() {
|
||||
return <ImgIcon alt="git" height="20" src={icon} width="20" />;
|
||||
return <img alt="git" height="20" src={icon} width="20" />;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const icon = require('./nodejs.svg');
|
||||
|
||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginLeft: theme?.spacing(1),
|
||||
}));
|
||||
|
||||
export function NodeJS() {
|
||||
return <ImgIcon alt="nodejs" height="20" src={icon} width="20" />;
|
||||
return <img alt="nodejs" height="20" src={icon} width="20" />;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const icon = require('./typescript.svg');
|
||||
|
||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginLeft: theme?.spacing(1),
|
||||
}));
|
||||
|
||||
export function TypeScript() {
|
||||
return <ImgIcon alt="typescript" height="20" src={icon} width="20" />;
|
||||
return <img alt="typescript" height="20" src={icon} width="20" />;
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
@ -9,7 +9,8 @@ const FileBinary = React.forwardRef(function FileBinary(
|
|||
ref: React.Ref<SVGSVGElement>
|
||||
) {
|
||||
return (
|
||||
<SvgIcon {...props} ref={ref}>
|
||||
// eslint-disable-next-line verdaccio/jsx-spread
|
||||
<SvgIcon viewBox="0 0 14 16" {...props} ref={ref}>
|
||||
<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" />
|
||||
</SvgIcon>
|
||||
);
|
||||
|
|
|
@ -6,13 +6,17 @@ import {
|
|||
CommonJS,
|
||||
ES6Modules,
|
||||
Earth,
|
||||
FileBinary,
|
||||
Git,
|
||||
Law,
|
||||
License,
|
||||
NodeJS,
|
||||
Npm,
|
||||
Pnpm,
|
||||
Time,
|
||||
TypeScript,
|
||||
Version,
|
||||
Yarn,
|
||||
} from '.';
|
||||
|
||||
export default {
|
||||
|
@ -21,16 +25,28 @@ export default {
|
|||
|
||||
export const Icons: any = () => (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Stack spacing={2}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Npm />
|
||||
<Pnpm />
|
||||
<Yarn />
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<NodeJS />
|
||||
<Git />
|
||||
<Version />
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TypeScript />
|
||||
<Time />
|
||||
<License />
|
||||
<Law />
|
||||
<ES6Modules />
|
||||
<CommonJS />
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Version />
|
||||
<Time />
|
||||
<FileBinary />
|
||||
<Law />
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<License />
|
||||
<Earth />
|
||||
</Stack>
|
||||
</Box>
|
||||
|
|
70
packages/ui-components/src/components/Icons/Icons.test.tsx
Normal file
70
packages/ui-components/src/components/Icons/Icons.test.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render } from '../../test/test-react-testing-library';
|
||||
import {
|
||||
CommonJS,
|
||||
ES6Modules,
|
||||
Earth,
|
||||
FileBinary,
|
||||
Git,
|
||||
Law,
|
||||
License,
|
||||
NodeJS,
|
||||
Npm,
|
||||
Pnpm,
|
||||
Time,
|
||||
TypeScript,
|
||||
Version,
|
||||
Yarn,
|
||||
} from './';
|
||||
import { SvgIcon } from './SvgIcon';
|
||||
|
||||
describe('Icon components', () => {
|
||||
test('should render an SVG graphic', () => {
|
||||
const { container } = render(
|
||||
<>
|
||||
<Earth />
|
||||
<FileBinary />
|
||||
<Law />
|
||||
<License />
|
||||
<Time />
|
||||
<Version />
|
||||
</>
|
||||
);
|
||||
expect(container.querySelectorAll('svg')).toHaveLength(6);
|
||||
});
|
||||
|
||||
test('should render an IMG graphic linking to and SVG', () => {
|
||||
const { container } = render(
|
||||
<>
|
||||
<CommonJS />
|
||||
<ES6Modules />
|
||||
<Git />
|
||||
<NodeJS />
|
||||
<TypeScript />
|
||||
<Npm />
|
||||
<Pnpm />
|
||||
<Yarn />
|
||||
</>
|
||||
);
|
||||
expect(container.querySelectorAll('img')).toHaveLength(8);
|
||||
});
|
||||
|
||||
test('should render small graphic', () => {
|
||||
const { container } = render(
|
||||
<SvgIcon size={'sm'}>
|
||||
<circle cx="7" cy="7" r="7" />
|
||||
</SvgIcon>
|
||||
);
|
||||
expect(container.querySelector('svg')).toHaveStyle('width: 14px');
|
||||
});
|
||||
|
||||
test('should render medium graphic', () => {
|
||||
const { container } = render(
|
||||
<SvgIcon size={'md'}>
|
||||
<circle cx="7" cy="7" r="7" />
|
||||
</SvgIcon>
|
||||
);
|
||||
expect(container.querySelector('svg')).toHaveStyle('width: 18px');
|
||||
});
|
||||
});
|
|
@ -6,7 +6,8 @@ type Props = React.ComponentProps<typeof SvgIcon>;
|
|||
|
||||
const Law = React.forwardRef(function Law(props: Props, ref: React.Ref<SVGSVGElement>) {
|
||||
return (
|
||||
<SvgIcon {...props} ref={ref}>
|
||||
// eslint-disable-next-line verdaccio/jsx-spread
|
||||
<SvgIcon viewBox="0 0 14 16" {...props} ref={ref}>
|
||||
<path
|
||||
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"
|
||||
fillRule="evenodd"
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const icon = require('./npm.svg');
|
||||
|
||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginLeft: theme?.spacing(1),
|
||||
}));
|
||||
|
||||
export function Npm() {
|
||||
return <ImgIcon alt="npm package manager" height="20" src={icon} width="20" />;
|
||||
return <img alt="npm package manager" height="20" src={icon} width="20" />;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const icon = require('./pnpm.svg');
|
||||
|
||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginLeft: theme?.spacing(1),
|
||||
}));
|
||||
|
||||
export function Pnpm() {
|
||||
return <ImgIcon alt="pnpm package manager" height="20" src={icon} width="20" />;
|
||||
return <img alt="pnpm package manager" height="20" src={icon} width="20" />;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const icon = require('./yarn.svg');
|
||||
|
||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginLeft: theme?.spacing(1),
|
||||
}));
|
||||
|
||||
export function Yarn() {
|
||||
return <ImgIcon alt="npm package manager" height="20" src={icon} width="20" />;
|
||||
return <img alt="npm package manager" height="20" src={icon} width="20" />;
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ 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 InstallListItem, { DependencyManager, getGlobalInstall } from './InstallListItem';
|
||||
import data from './__partials__/data.json';
|
||||
|
||||
const ComponentToBeRendered: React.FC<{ pkgManagers?: PackageManagers[] }> = () => {
|
||||
const ComponentToBeRendered: React.FC<{ name?: string; pkgManagers?: PackageManagers[] }> = ({
|
||||
name = 'foo',
|
||||
}) => {
|
||||
const { configOptions } = useConfig();
|
||||
return <Install configOptions={configOptions} packageMeta={data} packageName="foo" />;
|
||||
return <Install configOptions={configOptions} packageMeta={data} packageName={name} />;
|
||||
};
|
||||
|
||||
/* eslint-disable react/jsx-no-bind*/
|
||||
|
@ -22,6 +24,11 @@ describe('<Install />', () => {
|
|||
expect(screen.getByText('npm install foo@8.0.0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render if name is missing', () => {
|
||||
render(<ComponentToBeRendered name="" />);
|
||||
expect(screen.queryByTestId('installList')).toBeNull();
|
||||
});
|
||||
|
||||
test('should have 3 children', () => {
|
||||
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['yarn', 'pnpm', 'npm'];
|
||||
const { getByTestId } = render(<ComponentToBeRendered />);
|
||||
|
@ -32,9 +39,7 @@ describe('<Install />', () => {
|
|||
|
||||
test('should have the element NPM', () => {
|
||||
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['npm'];
|
||||
|
||||
render(<ComponentToBeRendered />);
|
||||
|
||||
expect(screen.getByText('sidebar.installation.title')).toBeTruthy();
|
||||
expect(screen.queryByText('pnpm')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('yarn')).not.toBeInTheDocument();
|
||||
|
@ -59,18 +64,54 @@ describe('<Install />', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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');
|
||||
describe('<InstallListItem />', () => {
|
||||
test('renders correctly', () => {
|
||||
render(
|
||||
<InstallListItem
|
||||
dependencyManager={DependencyManager.NPM}
|
||||
packageName={'foo'}
|
||||
packageVersion={'8.0.0'}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId('installListItem-npm')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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');
|
||||
test('should not render if name is missing', () => {
|
||||
render(
|
||||
// @ts-ignore - testing invalid value
|
||||
<InstallListItem dependencyManager={'other'} packageName={'foo'} packageVersion={'8.0.0'} />
|
||||
);
|
||||
// expect nothing to be rendered
|
||||
expect(screen.queryByTestId('installListItem-npm')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGlobalInstall', () => {
|
||||
test('version', () => {
|
||||
expect(getGlobalInstall(false, false, '1.0.0', 'foo')).toEqual('foo@1.0.0');
|
||||
});
|
||||
test('latest', () => {
|
||||
expect(getGlobalInstall(true, false, '1.0.0', 'foo')).toEqual('foo');
|
||||
});
|
||||
|
||||
test('version global', () => {
|
||||
expect(getGlobalInstall(false, true, '1.0.0', 'foo')).toEqual('-g foo@1.0.0');
|
||||
});
|
||||
test('latest global', () => {
|
||||
expect(getGlobalInstall(true, true, '1.0.0', 'foo')).toEqual('-g foo');
|
||||
});
|
||||
|
||||
test('yarn version', () => {
|
||||
expect(getGlobalInstall(false, false, '1.0.0', 'foo', true)).toEqual('foo@1.0.0');
|
||||
});
|
||||
test('yarn latest', () => {
|
||||
expect(getGlobalInstall(true, false, '1.0.0', 'foo', true)).toEqual('foo');
|
||||
});
|
||||
|
||||
test('yarn version global', () => {
|
||||
expect(getGlobalInstall(false, true, '1.0.0', 'foo', true)).toEqual('foo@1.0.0');
|
||||
});
|
||||
test('yarn latest global', () => {
|
||||
expect(getGlobalInstall(true, true, '1.0.0', 'foo', true)).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { Typography } from '@mui/material';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import List from '@mui/material/List';
|
||||
import { useTheme } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -18,6 +16,12 @@ const StyledText = styled(Typography)<{ theme?: Theme }>((props) => ({
|
|||
textTransform: 'capitalize',
|
||||
}));
|
||||
|
||||
const Wrapper = styled('div')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
export type Props = {
|
||||
packageMeta: PackageMetaInterface;
|
||||
packageName: string;
|
||||
|
@ -26,7 +30,6 @@ export type Props = {
|
|||
|
||||
const Install: React.FC<Props> = ({ packageMeta, packageName, configOptions }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
if (!packageMeta || !packageName) {
|
||||
return null;
|
||||
}
|
||||
|
@ -38,16 +41,14 @@ const Install: React.FC<Props> = ({ packageMeta, packageName, configOptions }) =
|
|||
|
||||
return hasPkgManagers ? (
|
||||
<>
|
||||
<Grid
|
||||
container={true}
|
||||
justifyContent="flex-end"
|
||||
sx={{ marginRight: theme.spacing(10), alingText: 'right' }}
|
||||
>
|
||||
<SettingsMenu packageName={packageName} />
|
||||
</Grid>
|
||||
<List
|
||||
data-testid={'installList'}
|
||||
subheader={<StyledText variant={'subtitle1'}>{t('sidebar.installation.title')}</StyledText>}
|
||||
subheader={
|
||||
<Wrapper>
|
||||
<StyledText variant={'subtitle1'}>{t('sidebar.installation.title')}</StyledText>
|
||||
<SettingsMenu packageName={packageName} />
|
||||
</Wrapper>
|
||||
}
|
||||
>
|
||||
{hasNpm && (
|
||||
<InstallListItem
|
||||
|
|
|
@ -22,9 +22,9 @@ const InstallListItemText = styled(ListItemText)({
|
|||
});
|
||||
|
||||
const PackageMangerAvatar = styled(Avatar)({
|
||||
borderRadius: '0px',
|
||||
backgroundColor: 'transparent',
|
||||
padding: 0,
|
||||
marginLeft: 0,
|
||||
});
|
||||
|
||||
export enum DependencyManager {
|
||||
|
@ -39,10 +39,10 @@ interface Interface {
|
|||
packageVersion?: string;
|
||||
}
|
||||
|
||||
export function getGlobalInstall(isGlobal, packageVersion, packageName, isYarn = false) {
|
||||
export function getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, isYarn = false) {
|
||||
const name = isGlobal
|
||||
? `${isYarn ? '' : '-g'} ${packageVersion ? `${packageName}@${packageVersion}` : packageName}`
|
||||
: packageVersion
|
||||
? `${isYarn ? '' : '-g'} ${packageVersion && !isLatest ? `${packageName}@${packageVersion}` : packageName}`
|
||||
: packageVersion && !isLatest
|
||||
? `${packageName}@${packageVersion}`
|
||||
: packageName;
|
||||
|
||||
|
@ -56,6 +56,7 @@ const InstallListItem: React.FC<Interface> = ({
|
|||
}) => {
|
||||
const { localSettings } = useSettings();
|
||||
const theme = useTheme();
|
||||
const isLatest = localSettings[packageName]?.latest ?? false;
|
||||
const isGlobal = localSettings[packageName]?.global ?? false;
|
||||
switch (dependencyManager) {
|
||||
case DependencyManager.NPM:
|
||||
|
@ -67,9 +68,9 @@ const InstallListItem: React.FC<Interface> = ({
|
|||
<InstallListItemText
|
||||
primary={
|
||||
<CopyToClipBoard
|
||||
dataTestId="instalNpm"
|
||||
text={`npm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
|
||||
title={`npm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
|
||||
dataTestId="installNpm"
|
||||
text={`npm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`}
|
||||
title={`npm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -93,7 +94,7 @@ const InstallListItem: React.FC<Interface> = ({
|
|||
packageName,
|
||||
true
|
||||
)}`
|
||||
: `yarn add ${getGlobalInstall(isGlobal, packageVersion, packageName, true)}`
|
||||
: `yarn add ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, true)}`
|
||||
}
|
||||
title={
|
||||
isGlobal
|
||||
|
@ -103,7 +104,7 @@ const InstallListItem: React.FC<Interface> = ({
|
|||
packageName,
|
||||
true
|
||||
)}`
|
||||
: `yarn add ${getGlobalInstall(isGlobal, packageVersion, packageName, true)}`
|
||||
: `yarn add ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, true)}`
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
@ -120,8 +121,8 @@ const InstallListItem: React.FC<Interface> = ({
|
|||
primary={
|
||||
<CopyToClipBoard
|
||||
dataTestId="installPnpm"
|
||||
text={`pnpm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
|
||||
title={`pnpm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
|
||||
text={`pnpm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`}
|
||||
title={`pnpm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import Chip from '@mui/material/Chip';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import React from 'react';
|
||||
|
||||
const KeywordListItems: React.FC<{ keywords: undefined | string | string[] }> = ({ keywords }) => {
|
||||
const keywordList =
|
||||
typeof keywords === 'string' ? keywords.replace(/,/g, ' ').split(' ') : keywords;
|
||||
|
||||
if (!keywordList) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem sx={{ px: 0, mt: 0, flexWrap: 'wrap' }}>
|
||||
{keywordList.sort().map((keyword, index) => (
|
||||
<Chip key={index} label={keyword} sx={{ mt: 1, mr: 1 }} />
|
||||
))}
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeywordListItems;
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import { default as Keywords } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Components/Sidebar/Keywords',
|
||||
};
|
||||
|
||||
export const AllProperties: any = () => (
|
||||
<Keywords
|
||||
packageMeta={{
|
||||
latest: {
|
||||
name: 'verdaccio1',
|
||||
version: '4.0.0',
|
||||
keywords: ['verdaccio', 'npm', 'yarn'],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../test/test-react-testing-library';
|
||||
import Keywords from './Keywords';
|
||||
|
||||
describe('<Keywords /> component', () => {
|
||||
test('should render the component in default state', () => {
|
||||
const packageMeta = {
|
||||
latest: {
|
||||
name: 'verdaccio1',
|
||||
version: '4.0.0',
|
||||
keywords: ['verdaccio', 'npm', 'yarn'],
|
||||
},
|
||||
};
|
||||
|
||||
const container = render(<Keywords packageMeta={packageMeta} />);
|
||||
|
||||
expect(container.getByText('sidebar.keywords.title')).toBeInTheDocument();
|
||||
expect(container.getByText('verdaccio')).toBeInTheDocument();
|
||||
expect(container.getByText('npm')).toBeInTheDocument();
|
||||
expect(container.getByText('yarn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render if data is missing', () => {
|
||||
// @ts-ignore
|
||||
render(<Keywords packageMeta={{}} />);
|
||||
expect(screen.queryByTestId('keyword-list')).toBeNull();
|
||||
|
||||
const packageMeta = {
|
||||
latest: {
|
||||
name: 'verdaccio1',
|
||||
version: '4.0.0',
|
||||
keywords: '',
|
||||
},
|
||||
};
|
||||
|
||||
render(<Keywords packageMeta={packageMeta} />);
|
||||
expect(screen.queryByTestId('keyword-list')).toBeNull();
|
||||
});
|
||||
|
||||
test('should render keywords set in string', () => {
|
||||
const packageMeta = {
|
||||
latest: {
|
||||
name: 'verdaccio1',
|
||||
version: '4.0.0',
|
||||
keywords: 'hello, world, verdaccio',
|
||||
},
|
||||
};
|
||||
|
||||
const container = render(<Keywords packageMeta={packageMeta} />);
|
||||
|
||||
expect(container.getByText('sidebar.keywords.title')).toBeInTheDocument();
|
||||
expect(container.getByText('verdaccio')).toBeInTheDocument();
|
||||
expect(container.getByText('hello')).toBeInTheDocument();
|
||||
expect(container.getByText('world')).toBeInTheDocument();
|
||||
});
|
||||
});
|
35
packages/ui-components/src/components/Keywords/Keywords.tsx
Normal file
35
packages/ui-components/src/components/Keywords/Keywords.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import List from '@mui/material/List';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTheme } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PackageMetaInterface } from '../../types/packageMeta';
|
||||
import KeywordListItems from './KeywordListItems';
|
||||
|
||||
const Keywords: React.FC<{ packageMeta: PackageMetaInterface }> = ({ packageMeta }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
if (!packageMeta?.latest?.keywords) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
data-testid="keyword-list"
|
||||
subheader={
|
||||
<Typography
|
||||
sx={{ fontWeight: theme.fontWeight.bold, textTransform: 'capitalize' }}
|
||||
variant="subtitle1"
|
||||
>
|
||||
{t('sidebar.keywords.title')}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<KeywordListItems keywords={packageMeta?.latest?.keywords} />
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default Keywords;
|
1
packages/ui-components/src/components/Keywords/index.ts
Normal file
1
packages/ui-components/src/components/Keywords/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './Keywords';
|
25
packages/ui-components/src/components/Link/Link.test.tsx
Normal file
25
packages/ui-components/src/components/Link/Link.test.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import { render } from '../../test/test-react-testing-library';
|
||||
import Link from './Link';
|
||||
|
||||
describe('<Link /> component', () => {
|
||||
test('should render the component in default state', () => {
|
||||
const { container } = render(
|
||||
<Router>
|
||||
<Link to={'/'} />
|
||||
</Router>
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render the component with link', () => {
|
||||
const { container } = render(
|
||||
<Router>
|
||||
<Link to={'/'}>{'Home'}</Link>
|
||||
</Router>
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -3,9 +3,7 @@ import Typography from '@mui/material/Typography';
|
|||
import React from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
type LinkRef = HTMLAnchorElement;
|
||||
|
||||
export const CustomRouterLink = styled(RouterLink)`
|
||||
const CustomRouterLink = styled(RouterLink)`
|
||||
text-decoration: none;
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
@ -13,23 +11,11 @@ export const CustomRouterLink = styled(RouterLink)`
|
|||
}
|
||||
`;
|
||||
|
||||
// TODO: improve any with custom types for a and RouterLink
|
||||
const Link = React.forwardRef<LinkRef, any>(function LinkFunction(
|
||||
{ external, to, children, variant, className, onClick },
|
||||
const Link = React.forwardRef<HTMLAnchorElement, any>(function LinkFunction(
|
||||
{ to, children, variant, className, onClick },
|
||||
ref
|
||||
) {
|
||||
return external ? (
|
||||
<a
|
||||
className={className}
|
||||
href={to}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Typography variant={variant ?? 'caption'}>{children}</Typography>
|
||||
</a>
|
||||
) : (
|
||||
return (
|
||||
<CustomRouterLink className={className} innerRef={ref} onClick={onClick} to={to}>
|
||||
<Typography variant={variant}>{children}</Typography>
|
||||
</CustomRouterLink>
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Link /> component should render the component in default state 1`] = `
|
||||
.emotion-0 {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-0:hover,
|
||||
.emotion-0:focus {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emotion-2 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
<a
|
||||
class="emotion-0 emotion-1"
|
||||
href="/"
|
||||
>
|
||||
<p
|
||||
class="MuiTypography-root MuiTypography-body1 emotion-2"
|
||||
/>
|
||||
</a>
|
||||
`;
|
||||
|
||||
exports[`<Link /> component should render the component with link 1`] = `
|
||||
.emotion-0 {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-0:hover,
|
||||
.emotion-0:focus {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emotion-2 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
<a
|
||||
class="emotion-0 emotion-1"
|
||||
href="/"
|
||||
>
|
||||
<p
|
||||
class="MuiTypography-root MuiTypography-body1 emotion-2"
|
||||
>
|
||||
Home
|
||||
</p>
|
||||
</a>
|
||||
`;
|
|
@ -1 +1 @@
|
|||
export { default as Link } from './Link';
|
||||
export { default } from './Link';
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render } from '../../test/test-react-testing-library';
|
||||
import LinkExternal from './LinkExternal';
|
||||
|
||||
describe('<LinkExternal /> component', () => {
|
||||
test('should render the component in default state', () => {
|
||||
const { container } = render(<LinkExternal to={'/'} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render the component with external link', () => {
|
||||
const { container } = render(
|
||||
<LinkExternal to={'https://example.com'}>{'Example'}</LinkExternal>
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
import Link from '@mui/material/Link';
|
||||
import React from 'react';
|
||||
|
||||
const LinkExternal = React.forwardRef<HTMLAnchorElement, any>((props, ref) => {
|
||||
const { to, children, variant, ...rest } = props;
|
||||
return (
|
||||
// eslint-disable-next-line verdaccio/jsx-spread
|
||||
<Link
|
||||
href={to}
|
||||
ref={ref}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
underline="hover"
|
||||
variant={variant ?? 'caption'}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
export default LinkExternal;
|
|
@ -0,0 +1,53 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LinkExternal /> component should render the component in default state 1`] = `
|
||||
.emotion-0 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.66;
|
||||
color: #4b5e40;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-0:hover {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-0"
|
||||
href="/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`<LinkExternal /> component should render the component with external link 1`] = `
|
||||
.emotion-0 {
|
||||
margin: 0;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.66;
|
||||
color: #4b5e40;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.emotion-0:hover {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
<a
|
||||
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-0"
|
||||
href="https://example.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Example
|
||||
</a>
|
||||
`;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './LinkExternal';
|
|
@ -1,12 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../test/test-react-testing-library';
|
||||
import { act, render, screen, waitFor } from '../../test/test-react-testing-library';
|
||||
import Loading from './Loading';
|
||||
|
||||
describe('<Loading /> component', () => {
|
||||
test('should render the component in default state', () => {
|
||||
render(<Loading />);
|
||||
screen.debug();
|
||||
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
||||
test('should render the component in default state', async () => {
|
||||
act(() => {
|
||||
render(<Loading />);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ export const Wrapper = styled('div')({
|
|||
|
||||
export const Badge = styled('div')<{ theme?: Theme }>(({ theme }) => ({
|
||||
margin: '0 0 30px 0',
|
||||
padding: 5,
|
||||
borderRadius: 25,
|
||||
boxShadow: '0 10px 20px 0 rgba(69, 58, 100, 0.2)',
|
||||
background: theme?.palette.mode === 'dark' ? theme?.palette.black : '#f7f8f6',
|
||||
|
|
|
@ -87,7 +87,7 @@ describe('<LoginDialog /> component', () => {
|
|||
fireEvent.change(userNameInput, { target: { value: 'xyz' } });
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('form-placeholder.password');
|
||||
expect(userNameInput).toBeInTheDocument();
|
||||
expect(passwordInput).toBeInTheDocument();
|
||||
fireEvent.focus(passwordInput);
|
||||
|
||||
await act(async () => {
|
||||
|
|
|
@ -21,7 +21,7 @@ const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
|
|||
const makeLogin = useCallback(
|
||||
async (username?: string, password?: string): Promise<LoginBody | void> => {
|
||||
// checks isEmpty
|
||||
if (isEmpty(username) || isEmpty(password)) {
|
||||
if (!username || !password || isEmpty(username) || isEmpty(password)) {
|
||||
dispatch.login.addError({
|
||||
type: 'error',
|
||||
description: i18next.t('form-validation.username-or-password-cant-be-empty'),
|
||||
|
@ -29,6 +29,15 @@ const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// checks min username and password length
|
||||
if (username.length < 2 || password.length < 2) {
|
||||
dispatch.login.addError({
|
||||
type: 'error',
|
||||
description: i18next.t('form-validation.required-min-length', { length: 2 }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dispatch.login.getUser({ username, password });
|
||||
// const response: LoginBody = await doLogin(username as string, password as string);
|
||||
|
|
|
@ -12,6 +12,7 @@ const StyledIconButton = styled(IconButton)<{ theme?: Theme }>(({ theme }) => ({
|
|||
right: theme.spacing() / 2,
|
||||
top: theme.spacing() / 2,
|
||||
color: theme.palette.grey[500],
|
||||
zIndex: 99,
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -48,7 +48,6 @@ const LoginDialogForm = memo(({ onSubmit, error }: Props) => {
|
|||
id="login--dialog-username"
|
||||
{...register('username', {
|
||||
required: { value: true, message: t('form-validation.required-field') },
|
||||
minLength: { value: 2, message: t('form-validation.required-min-length', { length: 2 }) },
|
||||
})}
|
||||
label={t('form.username')}
|
||||
margin="normal"
|
||||
|
@ -65,7 +64,6 @@ const LoginDialogForm = memo(({ onSubmit, error }: Props) => {
|
|||
id="login--dialog-password"
|
||||
{...register('password', {
|
||||
required: { value: true, message: t('form-validation.required-field') },
|
||||
minLength: { value: 2, message: t('form-validation.required-min-length', { length: 2 }) },
|
||||
})}
|
||||
data-testid="password"
|
||||
label={t('form.password')}
|
||||
|
|
|
@ -30,7 +30,7 @@ const LoginDialogFormError = memo(({ error }: Props) => {
|
|||
return (
|
||||
<StyledSnackbarContent
|
||||
message={
|
||||
<Box alignItems="center" display="flex">
|
||||
<Box alignItems="center" data-testid="error" display="flex">
|
||||
<StyledErrorIcon />
|
||||
{error.description}
|
||||
</Box>
|
||||
|
|
16
packages/ui-components/src/components/Logo/Logo.stories.tsx
Normal file
16
packages/ui-components/src/components/Logo/Logo.stories.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Box from '@mui/material/Box';
|
||||
import React from 'react';
|
||||
|
||||
import Logo from './Logo';
|
||||
|
||||
export default {
|
||||
title: 'Components/Logo',
|
||||
};
|
||||
|
||||
export const Icons: any = () => (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Logo size="x-small" />
|
||||
<Logo size="small" />
|
||||
<Logo size="big" />
|
||||
</Box>
|
||||
);
|
|
@ -1,11 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render } from '../../test/test-react-testing-library';
|
||||
import { render, screen } from '../../test/test-react-testing-library';
|
||||
import Logo from './Logo';
|
||||
|
||||
describe('<Logo /> component', () => {
|
||||
test('should render the component in default state', () => {
|
||||
const { container } = render(<Logo />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
expect(screen.getByTestId('default-logo')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should render custom logo', () => {
|
||||
window.__VERDACCIO_BASENAME_UI_OPTIONS.logo = 'custom.png';
|
||||
render(<Logo />);
|
||||
expect(screen.getByTestId('custom-logo')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { useTheme } from '@mui/styles';
|
||||
import React from 'react';
|
||||
|
||||
import { Theme, useConfig } from '../../';
|
||||
|
@ -22,18 +23,33 @@ interface Props {
|
|||
onClick?: () => void;
|
||||
className?: string;
|
||||
isDefault?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const Logo: React.FC<Props> = ({ size, onClick, className, isDefault = false }) => {
|
||||
const Logo: React.FC<Props> = ({ size, onClick, className, isDefault = false, title = '' }) => {
|
||||
const { configOptions } = useConfig();
|
||||
const theme = useTheme();
|
||||
if (!isDefault && configOptions?.logo) {
|
||||
const logoSrc =
|
||||
theme?.palette.mode === 'dark' && configOptions.logoDark
|
||||
? configOptions.logoDark
|
||||
: configOptions.logo;
|
||||
return (
|
||||
<ImageLogo className={className} onClick={onClick}>
|
||||
<img alt="logo" height="40px" src={configOptions.logo} />
|
||||
<img alt={title} data-testid={'custom-logo'} height="40px" src={logoSrc} />
|
||||
</ImageLogo>
|
||||
);
|
||||
}
|
||||
return <StyledLogo className={className} onClick={onClick} size={size} />;
|
||||
|
||||
return (
|
||||
<StyledLogo
|
||||
className={className}
|
||||
data-testid={'default-logo'}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
|
|
|
@ -17,5 +17,7 @@ exports[`<Logo /> component should render the component in default state 1`] = `
|
|||
|
||||
<div
|
||||
class="emotion-0 emotion-1"
|
||||
data-testid="default-logo"
|
||||
title=""
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -2,7 +2,13 @@ import React from 'react';
|
|||
import { MemoryRouter } from 'react-router';
|
||||
|
||||
import { store } from '../../';
|
||||
import { cleanup, renderWithStore } from '../../test/test-react-testing-library';
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
renderWithStore,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '../../test/test-react-testing-library';
|
||||
import Package from './Package';
|
||||
|
||||
/**
|
||||
|
@ -11,29 +17,36 @@ import Package from './Package';
|
|||
*/
|
||||
const dateOneMonthAgo = (): Date => new Date(1544377770747);
|
||||
|
||||
const props = {
|
||||
name: 'verdaccio',
|
||||
version: '1.0.0',
|
||||
time: String(dateOneMonthAgo()),
|
||||
license: 'MIT',
|
||||
description: 'Private NPM repository',
|
||||
author: {
|
||||
name: 'Sam',
|
||||
},
|
||||
dist: {
|
||||
fileCount: 1,
|
||||
unpackedSize: 171,
|
||||
tarball: 'http://localhost:9000/verdaccio/-/verdaccio-1.0.0.tgz',
|
||||
},
|
||||
keywords: ['verdaccio-core'],
|
||||
};
|
||||
|
||||
describe('<Package /> component', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('should load the component', () => {
|
||||
const props = {
|
||||
name: 'verdaccio',
|
||||
version: '1.0.0',
|
||||
time: String(dateOneMonthAgo()),
|
||||
license: 'MIT',
|
||||
description: 'Private NPM repository',
|
||||
author: {
|
||||
name: 'Sam',
|
||||
},
|
||||
keywords: ['verdaccio'],
|
||||
};
|
||||
|
||||
const wrapper = renderWithStore(
|
||||
<MemoryRouter>
|
||||
<Package
|
||||
author={props.author}
|
||||
description={props.description}
|
||||
dist={props.dist}
|
||||
keywords={props.keywords}
|
||||
license={props.license}
|
||||
name={props.name}
|
||||
time={props.time}
|
||||
|
@ -50,5 +63,25 @@ describe('<Package /> component', () => {
|
|||
expect(wrapper.getByText('MIT')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test.todo('should load the component without author');
|
||||
// test if click on download button will trigger the download action
|
||||
test('should download the package', async () => {
|
||||
renderWithStore(
|
||||
<MemoryRouter>
|
||||
<Package
|
||||
author={props.author}
|
||||
description={props.description}
|
||||
dist={props.dist}
|
||||
keywords={props.keywords}
|
||||
license={props.license}
|
||||
name={props.name}
|
||||
time={props.time}
|
||||
version={props.version}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
store
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('download-tarball'));
|
||||
await waitFor(() => expect(store.getState().loading.models.download).toBe(true));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,20 +3,20 @@ import styled from '@emotion/styled';
|
|||
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 List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { Dispatch, Link, RootState, Theme } from '../../';
|
||||
import { Dispatch, Link, LinkExternal, RootState, Theme } from '../../';
|
||||
import { FileBinary, Law, Time, Version } from '../../components/Icons';
|
||||
import { Author as PackageAuthor, PackageMetaInterface } from '../../types/packageMeta';
|
||||
import { url, utils } from '../../utils';
|
||||
import Tag from './Tag';
|
||||
import { Route, url, utils } from '../../utils';
|
||||
import KeywordListItems from '../Keywords/KeywordListItems';
|
||||
import {
|
||||
Author,
|
||||
Avatar,
|
||||
|
@ -28,7 +28,6 @@ import {
|
|||
PackageListItemText,
|
||||
PackageTitle,
|
||||
Published,
|
||||
TagContainer,
|
||||
Wrapper,
|
||||
WrapperLink,
|
||||
} from './styles';
|
||||
|
@ -47,7 +46,7 @@ export interface PackageInterface {
|
|||
time?: number | string;
|
||||
author: PackageAuthor;
|
||||
description?: string;
|
||||
keywords?: string[];
|
||||
keywords?: PackageMetaInterface['latest']['keywords'];
|
||||
license?: PackageMetaInterface['latest']['license'];
|
||||
homepage?: string;
|
||||
bugs?: Bugs;
|
||||
|
@ -61,7 +60,7 @@ const Package: React.FC<PackageInterface> = ({
|
|||
description,
|
||||
dist,
|
||||
homepage,
|
||||
keywords = [],
|
||||
keywords,
|
||||
license,
|
||||
name: packageName,
|
||||
time,
|
||||
|
@ -71,7 +70,6 @@ const Package: React.FC<PackageInterface> = ({
|
|||
const config = useSelector((state: RootState) => state.configuration.config);
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isLoading = useSelector((state: RootState) => state?.loading?.models.download);
|
||||
|
||||
const handleDownload = useCallback(
|
||||
|
@ -94,21 +92,12 @@ const Package: React.FC<PackageInterface> = ({
|
|||
const renderAuthorInfo = (): React.ReactNode => {
|
||||
const name = utils.getAuthorName(authorName);
|
||||
return (
|
||||
<Author>
|
||||
<Avatar alt={name} src={authorAvatar} />
|
||||
<Details>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: theme?.fontWeight.semiBold,
|
||||
color:
|
||||
theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</Details>
|
||||
</Author>
|
||||
<OverviewItem>
|
||||
<Author>
|
||||
<Avatar alt={name} src={authorAvatar} />
|
||||
<Details>{name}</Details>
|
||||
</Author>
|
||||
</OverviewItem>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -132,33 +121,34 @@ const Package: React.FC<PackageInterface> = ({
|
|||
time && (
|
||||
<OverviewItem>
|
||||
<StyledTime />
|
||||
<Published>{t('package.published-on', { time: utils.formatDate(time) })}</Published>
|
||||
{utils.formatDateDistance(time)}
|
||||
<Published title={utils.formatDate(time)}>
|
||||
{t('package.published-on', { time: utils.formatDateDistance(time) })}
|
||||
</Published>
|
||||
</OverviewItem>
|
||||
);
|
||||
|
||||
const renderHomePageLink = (): React.ReactNode =>
|
||||
homepage &&
|
||||
url.isURL(homepage) && (
|
||||
<Link external={true} to={homepage}>
|
||||
<LinkExternal to={homepage}>
|
||||
<Tooltip aria-label={t('package.homepage')} title={t('package.visit-home-page')}>
|
||||
<IconButton aria-label={t('package.homepage')} size="large">
|
||||
<HomeIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
</LinkExternal>
|
||||
);
|
||||
|
||||
const renderBugsLink = (): React.ReactNode =>
|
||||
bugs?.url &&
|
||||
url.isURL(bugs.url) && (
|
||||
<Link external={true} to={bugs.url}>
|
||||
<LinkExternal to={bugs.url}>
|
||||
<Tooltip aria-label={t('package.bugs')} title={t('package.open-an-issue')}>
|
||||
<IconButton aria-label={t('package.bugs')} size="large">
|
||||
<BugReport />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
</LinkExternal>
|
||||
);
|
||||
|
||||
const renderDownloadLink = (): React.ReactNode =>
|
||||
|
@ -174,7 +164,11 @@ const Package: React.FC<PackageInterface> = ({
|
|||
aria-label={t('package.download', { what: t('package.the-tar-file') })}
|
||||
title={t('package.tarball')}
|
||||
>
|
||||
<IconButton aria-label={t('package.download')} size="large">
|
||||
<IconButton
|
||||
aria-label={t('package.download')}
|
||||
data-testid="download-tarball"
|
||||
size="large"
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={13}>
|
||||
<DownloadIcon />
|
||||
|
@ -191,7 +185,7 @@ const Package: React.FC<PackageInterface> = ({
|
|||
return (
|
||||
<Grid container={true} item={true} xs={12}>
|
||||
<Grid item={true} xs={11}>
|
||||
<WrapperLink to={`/-/web/detail/${packageName}`}>
|
||||
<WrapperLink to={`${Route.DETAIL}${packageName}`}>
|
||||
<PackageTitle className="package-title" data-testid="package-title">
|
||||
{packageName}
|
||||
</PackageTitle>
|
||||
|
@ -213,15 +207,7 @@ const Package: React.FC<PackageInterface> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const renderSecondaryComponent = (): React.ReactNode => {
|
||||
const tags = keywords.sort().map((keyword, index) => <Tag key={index}>{keyword}</Tag>);
|
||||
return (
|
||||
<>
|
||||
<Description>{description}</Description>
|
||||
{tags.length > 0 && <TagContainer>{tags}</TagContainer>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderSecondaryComponent = (): React.ReactNode => <Description>{description}</Description>;
|
||||
|
||||
const renderPackageListItemText = (): React.ReactNode => (
|
||||
<PackageListItemText
|
||||
|
@ -231,10 +217,21 @@ const Package: React.FC<PackageInterface> = ({
|
|||
/>
|
||||
);
|
||||
|
||||
const renderKeywords = (): React.ReactNode => (
|
||||
<ListItem alignItems={'flex-start'} sx={{ py: 0, my: 0 }}>
|
||||
<List sx={{ p: 0, my: 0 }}>
|
||||
<KeywordListItems keywords={keywords} />
|
||||
</List>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper className={'package'} data-testid="package-item-list">
|
||||
<ListItem alignItems={'flex-start'}>{renderPackageListItemText()}</ListItem>
|
||||
<ListItem alignItems={'flex-start'}>
|
||||
<ListItem alignItems={'flex-start'} sx={{ mb: 0 }}>
|
||||
{renderPackageListItemText()}
|
||||
</ListItem>
|
||||
{keywords && keywords?.length > 0 ? renderKeywords() : null}
|
||||
<ListItem alignItems={'flex-start'} sx={{ mt: 0 }}>
|
||||
{renderAuthorInfo()}
|
||||
{renderVersionInfo()}
|
||||
{renderPublishedInfo()}
|
||||
|
@ -248,8 +245,8 @@ const Package: React.FC<PackageInterface> = ({
|
|||
export default Package;
|
||||
|
||||
const iconStyle = ({ theme }: { theme: Theme }) => css`
|
||||
margin: 2px 10px 0 0;
|
||||
fill: ${theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white};
|
||||
margin: 0 10px 0 0;
|
||||
fill: ${theme?.palette.mode === 'light' ? theme?.palette.greyDark : theme?.palette.white};
|
||||
`;
|
||||
|
||||
const StyledVersion = styled(Version)`
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render } from '../../../test/test-react-testing-library';
|
||||
import Tag from './Tag';
|
||||
|
||||
describe('<Tag /> component', () => {
|
||||
test('should load the component in default state', () => {
|
||||
const { container } = render(
|
||||
<Tag>
|
||||
<span>{'I am a child'}</span>
|
||||
</Tag>
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { Wrapper } from './styles';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Tag: React.FC<Props> = ({ children }) => <Wrapper>{children}</Wrapper>;
|
||||
|
||||
export default Tag;
|
|
@ -1,21 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Tag /> component should load the component in default state 1`] = `
|
||||
.emotion-0 {
|
||||
vertical-align: middle;
|
||||
line-height: 22px;
|
||||
border-radius: 2px;
|
||||
color: #485a3e;
|
||||
background-color: #f3f4f2;
|
||||
padding: 0.22rem 0.4rem;
|
||||
margin: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
<span
|
||||
class="emotion-0 emotion-1"
|
||||
>
|
||||
<span>
|
||||
I am a child
|
||||
</span>
|
||||
</span>
|
||||
`;
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Tag';
|
|
@ -1,11 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled('span')({
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '22px',
|
||||
borderRadius: '2px',
|
||||
color: '#485a3e',
|
||||
backgroundColor: '#f3f4f2',
|
||||
padding: '0.22rem 0.4rem',
|
||||
margin: '8px 8px 0 0',
|
||||
});
|
|
@ -11,8 +11,8 @@ import { Theme } from '../../';
|
|||
export const OverviewItem = styled('span')<{ theme?: Theme }>(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
margin: '0 0 0 16px',
|
||||
color: theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white,
|
||||
margin: '0 20px 0 0',
|
||||
color: theme?.palette.mode === 'light' ? theme?.palette.greyDark2 : theme?.palette.white,
|
||||
fontSize: 12,
|
||||
[`@media (max-width: ${theme?.breakPoints.medium}px)`]: {
|
||||
':nth-of-type(3)': {
|
||||
|
@ -26,10 +26,9 @@ export const OverviewItem = styled('span')<{ theme?: Theme }>(({ theme }) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
export const Published = styled('span')<{ theme?: Theme }>(({ theme }) => ({
|
||||
color: theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white,
|
||||
export const Published = styled('span')({
|
||||
margin: '0 5px 0 0',
|
||||
}));
|
||||
});
|
||||
|
||||
export const Details = styled('span')({
|
||||
marginLeft: '5px',
|
||||
|
@ -59,6 +58,7 @@ export const PackageTitle = styled('span')<{ theme?: Theme }>(({ theme }) => ({
|
|||
marginBottom: 12,
|
||||
color: theme?.palette.mode == 'dark' ? theme?.palette.dodgerBlue : theme?.palette.eclipse,
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none',
|
||||
[`@media (max-width: ${theme?.breakPoints.small}px)`]: {
|
||||
fontSize: 14,
|
||||
marginBottom: 8,
|
||||
|
@ -70,9 +70,9 @@ export const GridRightAligned = styled(Grid)({
|
|||
});
|
||||
|
||||
export const Wrapper = styled(List)<{ theme?: Theme }>(({ theme }) => ({
|
||||
':hover': {
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
theme?.palette?.type == 'dark' ? theme?.palette?.secondary.main : theme?.palette?.greyLight3,
|
||||
theme?.palette?.type == 'dark' ? theme?.palette?.secondary.main : theme?.palette?.greyLight2,
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -83,16 +83,6 @@ export const IconButton = styled(MuiIconButton)({
|
|||
},
|
||||
});
|
||||
|
||||
export const TagContainer = styled('span')<{ theme?: Theme }>(({ theme }) => ({
|
||||
marginTop: 8,
|
||||
marginBottom: 12,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
[`@media (max-width: ${theme?.breakPoints.medium}px)`]: {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
export const PackageListItemText = styled(ListItemText)({
|
||||
paddingRight: 0,
|
||||
});
|
||||
|
|
|
@ -40,6 +40,7 @@ describe('<PackageList /> component', () => {
|
|||
name: 'xyz',
|
||||
version: '1.1.0',
|
||||
description: 'xyz description',
|
||||
keywords: ['hello', 'mars'],
|
||||
author: { name: 'Martin', avatar: 'test avatar' },
|
||||
},
|
||||
],
|
||||
|
|
50
packages/ui-components/src/components/Person/Person.test.tsx
Normal file
50
packages/ui-components/src/components/Person/Person.test.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
|
||||
import { fireEvent, render, screen } from '../../test/test-react-testing-library';
|
||||
import { Developer } from '../../types/packageMeta';
|
||||
import Person from './Person';
|
||||
|
||||
const mockPerson: Developer = {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
url: 'https://example.com/~johndoe',
|
||||
};
|
||||
|
||||
const mockPackageName = 'test-package';
|
||||
const mockVersion = '1.0.0';
|
||||
|
||||
const ComponentToBeRendered: React.FC<{ withText?: boolean }> = ({ withText = false }) => {
|
||||
return (
|
||||
<Person
|
||||
packageName={mockPackageName}
|
||||
person={mockPerson}
|
||||
version={mockVersion}
|
||||
withText={withText}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Person component', () => {
|
||||
test('should render avatar', () => {
|
||||
render(<ComponentToBeRendered />);
|
||||
const avatar = screen.getByAltText(mockPerson.name);
|
||||
expect(avatar).toBeInTheDocument();
|
||||
// but not include text
|
||||
expect(screen.queryByText(mockPerson.name)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render name when withText is true', () => {
|
||||
render(<ComponentToBeRendered withText={true} />);
|
||||
const name = screen.getByText(mockPerson.name);
|
||||
expect(name).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render name when withText is false', async () => {
|
||||
render(<ComponentToBeRendered />);
|
||||
// hover over the avatar
|
||||
fireEvent.mouseEnter(screen.getByTestId(mockPerson.name));
|
||||
// wait for the tooltip to appear
|
||||
await screen.findByTestId(mockPerson.name + '-tooltip');
|
||||
});
|
||||
});
|
45
packages/ui-components/src/components/Person/Person.tsx
Normal file
45
packages/ui-components/src/components/Person/Person.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { Typography } from '@mui/material';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React from 'react';
|
||||
|
||||
import { Developer } from '../../types/packageMeta';
|
||||
import LinkExternal from '../LinkExternal';
|
||||
import PersonTooltip from './PersonTooltip';
|
||||
import { getLink, getName } from './utils';
|
||||
|
||||
const Person: React.FC<{
|
||||
person: Developer;
|
||||
packageName: string;
|
||||
version: string;
|
||||
withText?: boolean;
|
||||
}> = ({ person, packageName, version, withText = false }) => {
|
||||
const link = getLink(person, packageName, version);
|
||||
|
||||
const avatarComponent = (
|
||||
<Avatar alt={person.name} src={person.avatar} sx={{ width: 40, height: 40, ml: 0, mr: 1 }} />
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
data-testid={person.name}
|
||||
key={person.email}
|
||||
title={<PersonTooltip person={person} />}
|
||||
>
|
||||
{link.length > 0 ? (
|
||||
<LinkExternal to={link}>{avatarComponent}</LinkExternal>
|
||||
) : (
|
||||
avatarComponent
|
||||
)}
|
||||
</Tooltip>
|
||||
{withText && (
|
||||
<Typography sx={{ ml: 1 }} variant="subtitle2">
|
||||
{getName(person.name)}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Person;
|
|
@ -0,0 +1,26 @@
|
|||
import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
|
||||
import { Developer } from '../../types/packageMeta';
|
||||
import { url } from '../../utils';
|
||||
import LinkExternal from '../LinkExternal';
|
||||
|
||||
const PersonTooltip: React.FC<{ person: Developer }> = ({ person }) => (
|
||||
<Typography data-testid={person.name + '-tooltip'}>
|
||||
{person.name}
|
||||
{person.email && url.isEmail(person.email) && (
|
||||
<LinkExternal to={`mailto:${person.email}`}>
|
||||
<br />
|
||||
{person.email}
|
||||
</LinkExternal>
|
||||
)}
|
||||
{person.url && url.isURL(person.url) && (
|
||||
<LinkExternal to={person.url}>
|
||||
<br />
|
||||
{person.url}
|
||||
</LinkExternal>
|
||||
)}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
export default PersonTooltip;
|
1
packages/ui-components/src/components/Person/index.ts
Normal file
1
packages/ui-components/src/components/Person/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './Person';
|
20
packages/ui-components/src/components/Person/utils.ts
Normal file
20
packages/ui-components/src/components/Person/utils.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import i18next from 'i18next';
|
||||
|
||||
import { Developer } from '../../types/packageMeta';
|
||||
import { url } from '../../utils';
|
||||
|
||||
export function getLink(person: Developer, packageName: string, version: string): string {
|
||||
return person.email && url.isEmail(person.email)
|
||||
? `mailto:${person.email}?subject=${packageName} v${version}`
|
||||
: person.url && url.isURL(person.url)
|
||||
? person.url
|
||||
: '';
|
||||
}
|
||||
|
||||
export function getName(name?: string): string {
|
||||
return !name
|
||||
? i18next.t('author-unknown')
|
||||
: name.toLowerCase() === 'anonymous'
|
||||
? i18next.t('author-anonymous')
|
||||
: name;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue