mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-11-13 03:35:52 +01: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
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
|
# https://verdaccio.org/docs/webui
|
||||||
web:
|
web:
|
||||||
title: Verdaccio
|
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
|
# comment out to disable gravatar support
|
||||||
# gravatar: false
|
# gravatar: false
|
||||||
# by default packages are ordercer ascendant (asc|desc)
|
# by default packages are ordercer ascendant (asc|desc)
|
||||||
@ -35,6 +41,7 @@ web:
|
|||||||
# showSearch: true
|
# showSearch: true
|
||||||
# showRaw: true
|
# showRaw: true
|
||||||
# showDownloadTarball: true
|
# showDownloadTarball: true
|
||||||
|
# showUplinks: true
|
||||||
# HTML tags injected after manifest <scripts/>
|
# HTML tags injected after manifest <scripts/>
|
||||||
# scriptsBodyAfter:
|
# scriptsBodyAfter:
|
||||||
# - '<script type="text/javascript" src="https://my.company.com/customJS.min.js"></script>'
|
# - '<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
|
# https://verdaccio.org/docs/webui
|
||||||
web:
|
web:
|
||||||
title: Verdaccio
|
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
|
# Comment out to disable gravatar support
|
||||||
# gravatar: false
|
# gravatar: false
|
||||||
# By default packages are ordered ascendant (asc|desc)
|
# By default packages are ordered ascendant (asc|desc)
|
||||||
@ -38,6 +44,7 @@ web:
|
|||||||
# showSearch: true
|
# showSearch: true
|
||||||
# showRaw: true
|
# showRaw: true
|
||||||
# showDownloadTarball: true
|
# showDownloadTarball: true
|
||||||
|
# showUplinks: true
|
||||||
# HTML tags injected after manifest <scripts/>
|
# HTML tags injected after manifest <scripts/>
|
||||||
# scriptsBodyAfter:
|
# scriptsBodyAfter:
|
||||||
# - '<script type="text/javascript" src="https://my.company.com/customJS.min.js"></script>'
|
# - '<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 = {
|
export type CommonWebConf = {
|
||||||
title?: string;
|
title?: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
|
logoDark?: string;
|
||||||
favicon?: string;
|
favicon?: string;
|
||||||
gravatar?: boolean;
|
gravatar?: boolean;
|
||||||
sort_packages?: string;
|
sort_packages?: string;
|
||||||
@ -98,6 +99,7 @@ export type CommonWebConf = {
|
|||||||
showFooter?: boolean;
|
showFooter?: boolean;
|
||||||
showThemeSwitch?: boolean;
|
showThemeSwitch?: boolean;
|
||||||
showDownloadTarball?: boolean;
|
showDownloadTarball?: boolean;
|
||||||
|
showUplinks?: boolean;
|
||||||
hideDeprecatedVersions?: boolean;
|
hideDeprecatedVersions?: boolean;
|
||||||
primaryColor: string;
|
primaryColor: string;
|
||||||
showRaw?: boolean;
|
showRaw?: boolean;
|
||||||
|
@ -42,10 +42,11 @@ export function renderWebMiddleware(config, tokenMiddleware, pluginOptions) {
|
|||||||
res.sendFile(file, sendFileCallback(next));
|
res.sendFile(file, sendFileCallback(next));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function renderLogo(logo: string | undefined): string | undefined {
|
||||||
// check the origin of the logo
|
// check the origin of the logo
|
||||||
if (config?.web?.logo && !isURLhasValidProtocol(config?.web?.logo)) {
|
if (logo && !isURLhasValidProtocol(logo)) {
|
||||||
// URI related to a local file
|
// URI related to a local file
|
||||||
const absoluteLocalFile = path.posix.resolve(config.web.logo);
|
const absoluteLocalFile = path.posix.resolve(logo);
|
||||||
debug('serve local logo %s', absoluteLocalFile);
|
debug('serve local logo %s', absoluteLocalFile);
|
||||||
try {
|
try {
|
||||||
// TODO: replace existsSync by async alternative
|
// TODO: replace existsSync by async alternative
|
||||||
@ -55,22 +56,33 @@ export function renderWebMiddleware(config, tokenMiddleware, pluginOptions) {
|
|||||||
) {
|
) {
|
||||||
// Note: `path.join` will break on Windows, because it transforms `/` to `\`
|
// Note: `path.join` will break on Windows, because it transforms `/` to `\`
|
||||||
// Use POSIX version `path.posix.join` instead.
|
// Use POSIX version `path.posix.join` instead.
|
||||||
config.web.logo = path.posix.join('/-/static/', path.basename(config.web.logo));
|
logo = path.posix.join('/-/static/', path.basename(logo));
|
||||||
router.get(config.web.logo, function (_req, res, next) {
|
router.get(logo, function (_req, res, next) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
debug('serve custom logo web:%s - local:%s', config.web.logo, absoluteLocalFile);
|
debug('serve custom logo web:%s - local:%s', logo, absoluteLocalFile);
|
||||||
res.sendFile(absoluteLocalFile, sendFileCallback(next));
|
res.sendFile(absoluteLocalFile, sendFileCallback(next));
|
||||||
});
|
});
|
||||||
debug('enabled custom logo %s', config.web.logo);
|
debug('enabled custom logo %s', logo);
|
||||||
} else {
|
} else {
|
||||||
config.web.logo = undefined;
|
logo = undefined;
|
||||||
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
|
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
config.web.logo = undefined;
|
logo = undefined;
|
||||||
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
|
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) {
|
router.get('/-/web/:section/*', function (req, res) {
|
||||||
renderHTML(config, manifest, manifestFiles, req, res);
|
renderHTML(config, manifest, manifestFiles, req, res);
|
||||||
|
@ -26,16 +26,20 @@ const defaultManifestFiles: Manifest = {
|
|||||||
css: [],
|
css: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolveLogo(config: ConfigYaml, requestOptions: RequestOptions) {
|
export function resolveLogo(
|
||||||
if (typeof config?.web?.logo !== 'string') {
|
logo: string | undefined,
|
||||||
|
url_prefix: string | undefined,
|
||||||
|
requestOptions: RequestOptions
|
||||||
|
) {
|
||||||
|
if (typeof logo !== 'string') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const isLocalFile = config?.web?.logo && !isURLhasValidProtocol(config?.web?.logo);
|
const isLocalFile = logo && !isURLhasValidProtocol(logo);
|
||||||
|
|
||||||
if (isLocalFile) {
|
if (isLocalFile) {
|
||||||
return `${getPublicUrl(config?.url_prefix, requestOptions)}-/static/${path.basename(config?.web?.logo)}`;
|
return `${getPublicUrl(url_prefix, requestOptions)}-/static/${path.basename(logo)}`;
|
||||||
} else if (isURLhasValidProtocol(config?.web?.logo)) {
|
} else if (isURLhasValidProtocol(logo)) {
|
||||||
return config?.web?.logo;
|
return logo;
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -61,7 +65,8 @@ export default function renderHTML(
|
|||||||
const title = config?.web?.title ?? WEB_TITLE;
|
const title = config?.web?.title ?? WEB_TITLE;
|
||||||
const login = hasLogin(config);
|
const login = hasLogin(config);
|
||||||
const scope = config?.web?.scope ?? '';
|
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 pkgManagers = config?.web?.pkgManagers ?? ['yarn', 'pnpm', 'npm'];
|
||||||
const version = res.locals.app_version ?? '';
|
const version = res.locals.app_version ?? '';
|
||||||
const flags = {
|
const flags = {
|
||||||
@ -81,6 +86,8 @@ export default function renderHTML(
|
|||||||
showFooter,
|
showFooter,
|
||||||
showSearch,
|
showSearch,
|
||||||
showDownloadTarball,
|
showDownloadTarball,
|
||||||
|
showRaw,
|
||||||
|
showUplinks,
|
||||||
} = Object.assign(
|
} = Object.assign(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@ -97,6 +104,8 @@ export default function renderHTML(
|
|||||||
showFooter,
|
showFooter,
|
||||||
showSearch,
|
showSearch,
|
||||||
showDownloadTarball,
|
showDownloadTarball,
|
||||||
|
showRaw,
|
||||||
|
showUplinks,
|
||||||
darkMode,
|
darkMode,
|
||||||
url_prefix,
|
url_prefix,
|
||||||
basename,
|
basename,
|
||||||
@ -104,6 +113,7 @@ export default function renderHTML(
|
|||||||
primaryColor,
|
primaryColor,
|
||||||
version,
|
version,
|
||||||
logo,
|
logo,
|
||||||
|
logoDark,
|
||||||
flags,
|
flags,
|
||||||
login,
|
login,
|
||||||
pkgManagers,
|
pkgManagers,
|
||||||
|
@ -14,6 +14,7 @@ web:
|
|||||||
showRaw: true
|
showRaw: true
|
||||||
primary_color: '#ffffff'
|
primary_color: '#ffffff'
|
||||||
logo: './test/config/dark-logo.png'
|
logo: './test/config/dark-logo.png'
|
||||||
|
logoDark: './test/config/dark-logo.png'
|
||||||
html_cache: false
|
html_cache: false
|
||||||
|
|
||||||
url_prefix: /prefix
|
url_prefix: /prefix
|
||||||
|
@ -79,6 +79,13 @@ describe('test web server', () => {
|
|||||||
return loadLogo('file-logo.yaml', '/-/static/dark-logo.png');
|
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 () => {
|
test('should not render logo as absolute file is wrong', async () => {
|
||||||
const {
|
const {
|
||||||
window: { __VERDACCIO_BASENAME_UI_OPTIONS },
|
window: { __VERDACCIO_BASENAME_UI_OPTIONS },
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
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';
|
import { store } from '@verdaccio/ui-components';
|
||||||
|
|
||||||
@ -13,17 +13,21 @@ jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600);
|
|||||||
/* eslint-disable react/jsx-no-bind*/
|
/* eslint-disable react/jsx-no-bind*/
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
describe('footer', () => {
|
describe('footer', () => {
|
||||||
test('should display the Header component', () => {
|
test('should display the Header component', async () => {
|
||||||
|
await act(() => {
|
||||||
renderWithStore(<App />, store);
|
renderWithStore(<App />, store);
|
||||||
|
});
|
||||||
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not display the Header component', () => {
|
test('should not display the Header component', async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
|
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
|
||||||
showFooter: false,
|
showFooter: false,
|
||||||
};
|
};
|
||||||
|
await act(() => {
|
||||||
renderWithStore(<App />, store);
|
renderWithStore(<App />, store);
|
||||||
|
});
|
||||||
expect(screen.queryByTestId('footer')).toBeFalsy();
|
expect(screen.queryByTestId('footer')).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
"visit-home-page": "Visit homepage",
|
"visit-home-page": "Visit homepage",
|
||||||
"open-an-issue": "Open an issue",
|
"open-an-issue": "Open an issue",
|
||||||
"download-tarball": "Download tarball",
|
"download-tarball": "Download tarball",
|
||||||
"raw": "Raw Manifest"
|
"raw": "View manifest",
|
||||||
|
"raw-title": "Manifest of {{package}}"
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"registry-info": {
|
"registry-info": {
|
||||||
@ -59,7 +60,7 @@
|
|||||||
"hide-deprecated": "All deprecated versions are hidden by global configuration"
|
"hide-deprecated": "All deprecated versions are hidden by global configuration"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"published-on": "Published on {{time}} •",
|
"published-on": "Published {{time}}",
|
||||||
"version": "v{{version}}",
|
"version": "v{{version}}",
|
||||||
"visit-home-page": "Visit homepage",
|
"visit-home-page": "Visit homepage",
|
||||||
"homepage": "Homepage",
|
"homepage": "Homepage",
|
||||||
@ -84,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
"form-validation": {
|
"form-validation": {
|
||||||
"required-field": "This field is required",
|
"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",
|
"unable-to-sign-in": "Unable to sign in",
|
||||||
"username-or-password-cant-be-empty": "Username or password can't be empty!"
|
"username-or-password-cant-be-empty": "Username or password can't be empty!"
|
||||||
},
|
},
|
||||||
@ -105,6 +106,7 @@
|
|||||||
},
|
},
|
||||||
"installation": {
|
"installation": {
|
||||||
"title": "Installation",
|
"title": "Installation",
|
||||||
|
"latest": "latest version",
|
||||||
"global": "global package",
|
"global": "global package",
|
||||||
"yarnModern": "yarn modern syntax"
|
"yarnModern": "yarn modern syntax"
|
||||||
},
|
},
|
||||||
@ -115,10 +117,13 @@
|
|||||||
"title": "Author"
|
"title": "Author"
|
||||||
},
|
},
|
||||||
"distribution": {
|
"distribution": {
|
||||||
"title": "Latest Distribution",
|
"title": "Distribution",
|
||||||
"license": "License",
|
"license": "License",
|
||||||
"size": "Size",
|
"size": "Size",
|
||||||
"file-count": "file count"
|
"file-count": "File Count"
|
||||||
|
},
|
||||||
|
"keywords": {
|
||||||
|
"title": "Keywords"
|
||||||
},
|
},
|
||||||
"maintainers": {
|
"maintainers": {
|
||||||
"title": "Maintainers"
|
"title": "Maintainers"
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
"react/jsx-curly-brace-presence": ["warn", { "props": "ignore", "children": "ignore" }],
|
"react/jsx-curly-brace-presence": ["warn", { "props": "ignore", "children": "ignore" }],
|
||||||
"react/jsx-pascal-case": ["error"],
|
"react/jsx-pascal-case": ["error"],
|
||||||
"react/jsx-props-no-multi-spaces": ["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/jsx-sort-props": ["error"],
|
||||||
"react/no-string-refs": ["error"],
|
"react/no-string-refs": ["error"],
|
||||||
"react/no-danger-with-children": ["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';
|
import { rest } from 'msw';
|
||||||
|
|
||||||
const packagesPayload = require('./api/home-packages.json');
|
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) => {
|
rest.get('http://localhost:9000/-/verdaccio/data/package/readme/storybook', (req, res, ctx) => {
|
||||||
return res(
|
return res(ctx.text(require('./api/storybook-readme')()));
|
||||||
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>
|
|
||||||
`)
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/jquery', (req, res, ctx) => {
|
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/jquery', (req, res, ctx) => {
|
||||||
return res(ctx.json(require('./api/jquery-sidebar.json')));
|
return res(ctx.json(require('./api/jquery-sidebar.json')));
|
||||||
}),
|
}),
|
||||||
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/JSONStream', (req, res, ctx) => {
|
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) => {
|
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) => {
|
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) => {
|
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/glob', (req, res, ctx) => {
|
||||||
return res(ctx.json(require('./api/glob-sidebar.json')));
|
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) => {
|
rest.get('http://localhost:9000/-/verdaccio/data/package/readme/jquery', (req, res, ctx) => {
|
||||||
return res(ctx.text(require('./api/jquery-readme')()));
|
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 }>(
|
rest.post<{ username: string; password: string }, { token: string; username: string }>(
|
||||||
'http://localhost:9000/-/verdaccio/sec/login',
|
'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',
|
black: '#000',
|
||||||
white: '#fff',
|
white: '#fff',
|
||||||
red: '#d32f2f',
|
red: '#d32f2f',
|
||||||
orange: '#CD4000',
|
orange: '#cd4000',
|
||||||
greySuperLight: '#f5f5f5',
|
greySuperLight: '#f5f5f5',
|
||||||
greyLight: '#d3d3d3',
|
greyLight: '#d3d3d3',
|
||||||
greyLight2: '#908ba1',
|
greyLight2: '#908ba1',
|
||||||
greyLight3: '#f3f4f240',
|
greyLight3: '#f3f4f2',
|
||||||
greyDark: '#a9a9a9',
|
greyDark: '#a9a9a9',
|
||||||
greyDark2: '#586069',
|
greyDark2: '#586069',
|
||||||
greyChateau: '#95989a',
|
greyChateau: '#95989a',
|
||||||
@ -38,7 +38,7 @@ const themeModes = {
|
|||||||
...colors,
|
...colors,
|
||||||
primary: '#ffffff',
|
primary: '#ffffff',
|
||||||
secondary: '#424242',
|
secondary: '#424242',
|
||||||
background: '#1A202C',
|
background: '#1a202c',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,28 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { store } from '../../store/store';
|
import { store } from '../../store/store';
|
||||||
import {
|
import { cleanup, fireEvent, renderWithStore, screen } from '../../test/test-react-testing-library';
|
||||||
cleanup,
|
|
||||||
fireEvent,
|
|
||||||
renderWithStore,
|
|
||||||
screen,
|
|
||||||
waitFor,
|
|
||||||
} from '../../test/test-react-testing-library';
|
|
||||||
import ActionBar from './ActionBar';
|
import ActionBar from './ActionBar';
|
||||||
|
|
||||||
const defaultPackageMeta = {
|
const defaultPackageMeta = {
|
||||||
_uplinks: {},
|
_uplinks: {},
|
||||||
latest: {
|
latest: {
|
||||||
name: 'verdaccio-ui/local-storage',
|
name: 'verdaccio',
|
||||||
version: '8.0.1-next.1',
|
version: '1.0.0',
|
||||||
dist: {
|
dist: {
|
||||||
fileCount: 0,
|
fileCount: 1,
|
||||||
unpackedSize: 0,
|
unpackedSize: 171,
|
||||||
tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
|
tarball: 'http://localhost:9000/verdaccio/-/verdaccio-1.0.0.tgz',
|
||||||
},
|
},
|
||||||
homepage: 'https://verdaccio.org',
|
homepage: 'https://verdaccio.org',
|
||||||
bugs: {
|
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();
|
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', () => {
|
test('when there is no action bar data', () => {
|
||||||
const packageMeta = {
|
const packageMeta = {
|
||||||
...defaultPackageMeta,
|
...defaultPackageMeta,
|
||||||
@ -64,6 +64,14 @@ describe('<ActionBar /> component', () => {
|
|||||||
expect(screen.getByLabelText('action-bar-action.download-tarball')).toBeTruthy();
|
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', () => {
|
test('when there is a button to raw manifest', () => {
|
||||||
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
|
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
|
||||||
expect(screen.getByLabelText('action-bar-action.raw')).toBeTruthy();
|
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 () => {
|
test('when click button to raw manifest open a dialog with viewer', async () => {
|
||||||
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
|
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={true} />, store);
|
||||||
|
expect(screen.queryByTestId('rawViewer--dialog')).toBeFalsy();
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText('action-bar-action.raw'));
|
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', () => {
|
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();
|
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', () => {
|
test('should not display show raw button', () => {
|
||||||
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={false} />, store);
|
renderWithStore(<ActionBar packageMeta={defaultPackageMeta} showRaw={false} />, store);
|
||||||
expect(screen.queryByLabelText('action-bar-action.raw')).toBeFalsy();
|
expect(screen.queryByLabelText('action-bar-action.raw')).toBeFalsy();
|
||||||
|
@ -41,7 +41,7 @@ const ActionBar: React.FC<Props> = ({ showRaw, showDownloadTarball = true, packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box alignItems="center" display="flex" marginBottom="14px">
|
<Box alignItems="center" display="flex" sx={{ my: 2 }}>
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
{actions.map((action) => (
|
{actions.map((action) => (
|
||||||
<ActionBarAction key={action.type} {...action} />
|
<ActionBarAction key={action.type} {...action} />
|
||||||
|
@ -12,11 +12,14 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
|
|
||||||
import { Theme } from '../../Theme';
|
import { Theme } from '../../Theme';
|
||||||
import { Dispatch, RootState } from '../../store/store';
|
import { Dispatch, RootState } from '../../store/store';
|
||||||
import { Link } from '../Link';
|
import LinkExternal from '../LinkExternal';
|
||||||
|
|
||||||
export const Fab = styled(FabMUI)<{ theme?: Theme }>(({ theme }) => ({
|
export const Fab = styled(FabMUI)<{ theme?: Theme }>(({ theme }) => ({
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue,
|
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,
|
color: theme?.palette.white,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -42,21 +45,21 @@ const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link, action })
|
|||||||
case 'VISIT_HOMEPAGE':
|
case 'VISIT_HOMEPAGE':
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t('action-bar-action.visit-home-page') as string}>
|
<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">
|
<Fab size="small">
|
||||||
<HomeIcon />
|
<HomeIcon />
|
||||||
</Fab>
|
</Fab>
|
||||||
</Link>
|
</LinkExternal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
case 'OPEN_AN_ISSUE':
|
case 'OPEN_AN_ISSUE':
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t('action-bar-action.open-an-issue') as string}>
|
<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">
|
<Fab size="small">
|
||||||
<BugReportIcon />
|
<BugReportIcon />
|
||||||
</Fab>
|
</Fab>
|
||||||
</Link>
|
</LinkExternal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
case 'DOWNLOAD_TARBALL':
|
case 'DOWNLOAD_TARBALL':
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
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 { PackageMetaInterface } from '../../types/packageMeta';
|
||||||
import Authors from './Author';
|
import Authors from './Author';
|
||||||
|
|
||||||
@ -66,4 +66,10 @@ describe('<Author /> component', () => {
|
|||||||
const wrapper = render(withAuthorComponent(packageMeta));
|
const wrapper = render(withAuthorComponent(packageMeta));
|
||||||
expect(wrapper).toMatchSnapshot();
|
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 List from '@mui/material/List';
|
||||||
import { useTheme } from '@mui/styles';
|
|
||||||
import i18next from 'i18next';
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { url } from '../../utils';
|
import Person from '../Person';
|
||||||
import { AuthorListItem, StyledText } from './styles';
|
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 Author: FC<{ packageMeta }> = ({ packageMeta }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
if (!packageMeta) {
|
if (!packageMeta) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -40,26 +18,10 @@ const Author: FC<{ packageMeta }> = ({ packageMeta }) => {
|
|||||||
return null;
|
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 (
|
return (
|
||||||
<List subheader={<StyledText variant={'subtitle1'}>{t('sidebar.author.title')}</StyledText>}>
|
<List subheader={<StyledText variant={'subtitle1'}>{t('sidebar.author.title')}</StyledText>}>
|
||||||
<AuthorListItem>
|
<AuthorListItem sx={{ my: 1 }}>
|
||||||
{!email || !url.isEmail(email) ? (
|
<Person packageName={packageName} person={author} version={version} withText={true} />
|
||||||
avatarComponent
|
|
||||||
) : (
|
|
||||||
<a href={`mailto:${email}?subject=${packageName}@${version}`} target={'_top'}>
|
|
||||||
{avatarComponent}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{name && <Typography variant="subtitle2">{getAuthorName(name)}</Typography>}
|
|
||||||
</AuthorListItem>
|
</AuthorListItem>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
@ -44,6 +44,8 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +70,22 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emotion-5 {
|
.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;
|
position: relative;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
@ -97,10 +115,11 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
margin-left: 0px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-6 {
|
.emotion-7 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -109,12 +128,13 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||||||
text-indent: 10000px;
|
text-indent: 10000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-7 {
|
.emotion-8 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.57;
|
line-height: 1.57;
|
||||||
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
<body>
|
<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"
|
class="MuiListItem-root MuiListItem-gutters MuiListItem-padding emotion-3 emotion-4"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="mailto:verdaccio.user@verdaccio.org?subject=verdaccio@4.0.0"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-5"
|
||||||
target="_top"
|
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
|
<div
|
||||||
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
class="MuiAvatar-root MuiAvatar-circular emotion-6"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="verdaccio user"
|
alt="verdaccio user"
|
||||||
class="MuiAvatar-img emotion-6"
|
class="MuiAvatar-img emotion-7"
|
||||||
src="https://www.gravatar.com/avatar/000000"
|
src="https://www.gravatar.com/avatar/000000"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<h6
|
<h6
|
||||||
class="MuiTypography-root MuiTypography-subtitle2 emotion-7"
|
class="MuiTypography-root MuiTypography-subtitle2 emotion-8"
|
||||||
>
|
>
|
||||||
verdaccio user
|
verdaccio user
|
||||||
</h6>
|
</h6>
|
||||||
@ -194,6 +218,8 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,6 +244,22 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emotion-5 {
|
.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;
|
position: relative;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
@ -247,10 +289,11 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
margin-left: 0px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-6 {
|
.emotion-7 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -259,12 +302,13 @@ exports[`<Author /> component should render the component in default state 1`] =
|
|||||||
text-indent: 10000px;
|
text-indent: 10000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-7 {
|
.emotion-8 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.57;
|
line-height: 1.57;
|
||||||
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div>
|
<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"
|
class="MuiListItem-root MuiListItem-gutters MuiListItem-padding emotion-3 emotion-4"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="mailto:verdaccio.user@verdaccio.org?subject=verdaccio@4.0.0"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-5"
|
||||||
target="_top"
|
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
|
<div
|
||||||
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
class="MuiAvatar-root MuiAvatar-circular emotion-6"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="verdaccio user"
|
alt="verdaccio user"
|
||||||
class="MuiAvatar-img emotion-6"
|
class="MuiAvatar-img emotion-7"
|
||||||
src="https://www.gravatar.com/avatar/000000"
|
src="https://www.gravatar.com/avatar/000000"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<h6
|
<h6
|
||||||
class="MuiTypography-root MuiTypography-subtitle2 emotion-7"
|
class="MuiTypography-root MuiTypography-subtitle2 emotion-8"
|
||||||
>
|
>
|
||||||
verdaccio user
|
verdaccio user
|
||||||
</h6>
|
</h6>
|
||||||
@ -399,6 +447,8 @@ exports[`<Author /> component should render the component when there is no autho
|
|||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,6 +502,7 @@ exports[`<Author /> component should render the component when there is no autho
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
margin-left: 0px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,6 +521,7 @@ exports[`<Author /> component should render the component when there is no autho
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.57;
|
line-height: 1.57;
|
||||||
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -487,6 +539,8 @@ exports[`<Author /> component should render the component when there is no autho
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
||||||
|
data-mui-internal-clone-element="true"
|
||||||
|
data-testid="verdaccio user"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="verdaccio user"
|
alt="verdaccio user"
|
||||||
@ -544,6 +598,8 @@ exports[`<Author /> component should render the component when there is no autho
|
|||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -597,6 +653,7 @@ exports[`<Author /> component should render the component when there is no autho
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
margin-left: 0px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,6 +672,7 @@ exports[`<Author /> component should render the component when there is no autho
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.57;
|
line-height: 1.57;
|
||||||
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -631,6 +689,8 @@ exports[`<Author /> component should render the component when there is no autho
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
class="MuiAvatar-root MuiAvatar-circular emotion-5"
|
||||||
|
data-mui-internal-clone-element="true"
|
||||||
|
data-testid="verdaccio user"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="verdaccio user"
|
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: SyntheticEvent<HTMLElement>): void => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const node = document.createElement('div');
|
// document.execCommand is deprecated
|
||||||
node.innerText = str;
|
navigator.clipboard.writeText(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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
import CardContent from '@mui/material/CardContent';
|
import CardContent from '@mui/material/CardContent';
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Theme } from '../../Theme';
|
|
||||||
import NoItems from '../NoItems';
|
import NoItems from '../NoItems';
|
||||||
import { DependencyBlock } from './DependencyBlock';
|
import { DependencyBlock } from './DependencyBlock';
|
||||||
import { hasKeys } from './utits';
|
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 Dependencies: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -43,7 +37,7 @@ const Dependencies: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
|||||||
hasKeys(peerDependencies);
|
hasKeys(peerDependencies);
|
||||||
if (hasDependencies) {
|
if (hasDependencies) {
|
||||||
return (
|
return (
|
||||||
<CardWrap>
|
<Card sx={{ mb: 2 }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Box data-testid="dependencies-box" sx={{ m: 2 }}>
|
<Box data-testid="dependencies-box" sx={{ m: 2 }}>
|
||||||
{Object.entries(dependencyMap).map(([dependencyType, dependencies]) => {
|
{Object.entries(dependencyMap).map(([dependencyType, dependencies]) => {
|
||||||
@ -62,11 +56,17 @@ const Dependencies: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
|||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</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;
|
export default Dependencies;
|
||||||
|
@ -9,6 +9,7 @@ import { useHistory } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Theme } from '../../Theme';
|
import { Theme } from '../../Theme';
|
||||||
import { PackageDependencies } from '../../types/packageMeta';
|
import { PackageDependencies } from '../../types/packageMeta';
|
||||||
|
import { Route } from '../../utils';
|
||||||
|
|
||||||
interface DependencyBlockProps {
|
interface DependencyBlockProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -41,7 +42,7 @@ export const DependencyBlock: React.FC<DependencyBlockProps> = ({ title, depende
|
|||||||
const deps = Object.entries(dependencies);
|
const deps = Object.entries(dependencies);
|
||||||
|
|
||||||
function handleClick(name: string): void {
|
function handleClick(name: string): void {
|
||||||
history.push(`/-/web/detail/${name}`);
|
history.push(`${Route.DETAIL}${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function labelText(title: string, name: string, version: string): string {
|
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 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 Developers from './Developers';
|
||||||
import { DeveloperType } from './Title';
|
|
||||||
|
|
||||||
describe('test Developers', () => {
|
describe('test Developers', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -63,7 +63,7 @@ describe('test Developers', () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should test onClick the component avatar', () => {
|
test('should show only up to max items', () => {
|
||||||
const packageMeta = {
|
const packageMeta = {
|
||||||
latest: {
|
latest: {
|
||||||
packageName: 'foo',
|
packageName: 'foo',
|
||||||
@ -72,6 +72,7 @@ describe('test Developers', () => {
|
|||||||
{
|
{
|
||||||
name: 'dmethvin',
|
name: 'dmethvin',
|
||||||
email: 'test@gmail.com',
|
email: 'test@gmail.com',
|
||||||
|
url: 'https://example.com/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dmethvin2',
|
name: 'dmethvin2',
|
||||||
@ -89,14 +90,40 @@ describe('test Developers', () => {
|
|||||||
<Developers packageMeta={packageMeta} type={DeveloperType.CONTRIBUTORS} visibleMax={1} />
|
<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();
|
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();
|
test('renders only the first six contributors when there are more than six', () => {
|
||||||
expect(wrapper.getByLabelText(packageMeta.latest.contributors[1].name)).toBeInTheDocument();
|
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 styled from '@emotion/styled';
|
||||||
import Add from '@mui/icons-material/Add';
|
import Add from '@mui/icons-material/Add';
|
||||||
import Avatar from '@mui/material/Avatar';
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import FabMUI from '@mui/material/Fab';
|
import FabMUI from '@mui/material/Fab';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Theme } from '../../Theme';
|
import { Theme } from '../../Theme';
|
||||||
|
import Person from '../Person';
|
||||||
|
import { DeveloperType } from './DeveloperType';
|
||||||
import Title from './Title';
|
import Title from './Title';
|
||||||
import getUniqueDeveloperValues from './get-unique-developer-values';
|
import getUniqueDeveloperValues from './get-unique-developer-values';
|
||||||
|
|
||||||
export enum DeveloperType {
|
|
||||||
CONTRIBUTORS = 'contributors',
|
|
||||||
MAINTAINERS = 'maintainers',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Fab = styled(FabMUI)<{ theme?: Theme }>((props) => ({
|
export const Fab = styled(FabMUI)<{ theme?: Theme }>((props) => ({
|
||||||
backgroundColor: props.theme?.palette.primary.main,
|
backgroundColor: props.theme?.palette.primary.main,
|
||||||
color: props.theme?.palette.white,
|
color: props.theme?.palette.white,
|
||||||
@ -28,7 +23,7 @@ interface Props {
|
|||||||
|
|
||||||
const StyledBox = styled(Box)({
|
const StyledBox = styled(Box)({
|
||||||
'> *': {
|
'> *': {
|
||||||
margin: 5,
|
marginRight: 5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -58,19 +53,24 @@ const Developers: React.FC<Props> = ({ type, visibleMax = VISIBLE_MAX, packageMe
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { name: packageName, version } = packageMeta.latest;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title type={type} />
|
<Title type={type} />
|
||||||
<StyledBox display="flex" flexWrap="wrap" margin="10px 0 10px 0">
|
<StyledBox display="flex" flexWrap="wrap" margin="10px 0 10px 0">
|
||||||
{visibleDevelopers.map((visibleDeveloper) => {
|
{visibleDevelopers.map((visibleDeveloper, index) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip key={visibleDeveloper.email} title={visibleDeveloper.name}>
|
<Person
|
||||||
<Avatar alt={visibleDeveloper.name} src={visibleDeveloper.avatar} />
|
key={index}
|
||||||
</Tooltip>
|
packageName={packageName}
|
||||||
|
person={visibleDeveloper}
|
||||||
|
version={version}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{visibleDevelopersMax < developers.length && (
|
{visibleDevelopersMax < developers.length && (
|
||||||
<Fab onClick={handleSetVisibleDevelopersMax} size="small">
|
<Fab data-testid={'fab-add'} onClick={handleSetVisibleDevelopersMax} size="small">
|
||||||
<Add />
|
<Add />
|
||||||
</Fab>
|
</Fab>
|
||||||
)}
|
)}
|
||||||
|
@ -4,11 +4,7 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Theme } from '../../Theme';
|
import { Theme } from '../../Theme';
|
||||||
|
import { DeveloperType } from './';
|
||||||
export enum DeveloperType {
|
|
||||||
CONTRIBUTORS = 'contributors',
|
|
||||||
MAINTAINERS = 'maintainers',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: DeveloperType;
|
type: DeveloperType;
|
||||||
|
@ -26,10 +26,26 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emotion-3>* {
|
.emotion-3>* {
|
||||||
margin: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-4 {
|
.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;
|
position: relative;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
@ -59,9 +75,13 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
color: #f4f4f4;
|
color: #f4f4f4;
|
||||||
background-color: #bdbdbd;
|
background-color: #bdbdbd;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-5 {
|
.emotion-6 {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
@ -90,14 +110,20 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
<div
|
<div
|
||||||
class="emotion-2 MuiBox-root emotion-3"
|
class="emotion-2 MuiBox-root emotion-3"
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
aria-label="dmethvin"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
|
||||||
data-mui-internal-clone-element="true"
|
data-mui-internal-clone-element="true"
|
||||||
|
data-testid="dmethvin"
|
||||||
|
href="mailto:test@gmail.com?subject=undefined v1.0.0"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||||
data-testid="PersonIcon"
|
data-testid="PersonIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -107,14 +133,21 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</a>
|
||||||
aria-label="mgol"
|
<a
|
||||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||||
data-mui-internal-clone-element="true"
|
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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||||
data-testid="PersonIcon"
|
data-testid="PersonIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -124,6 +157,7 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>,
|
</body>,
|
||||||
@ -150,10 +184,26 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emotion-3>* {
|
.emotion-3>* {
|
||||||
margin: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-4 {
|
.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;
|
position: relative;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
@ -183,9 +233,13 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
color: #f4f4f4;
|
color: #f4f4f4;
|
||||||
background-color: #bdbdbd;
|
background-color: #bdbdbd;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-5 {
|
.emotion-6 {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
@ -213,14 +267,20 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
<div
|
<div
|
||||||
class="emotion-2 MuiBox-root emotion-3"
|
class="emotion-2 MuiBox-root emotion-3"
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
aria-label="dmethvin"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
|
||||||
data-mui-internal-clone-element="true"
|
data-mui-internal-clone-element="true"
|
||||||
|
data-testid="dmethvin"
|
||||||
|
href="mailto:test@gmail.com?subject=undefined v1.0.0"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||||
data-testid="PersonIcon"
|
data-testid="PersonIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -230,14 +290,21 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</a>
|
||||||
aria-label="mgol"
|
<a
|
||||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||||
data-mui-internal-clone-element="true"
|
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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||||
data-testid="PersonIcon"
|
data-testid="PersonIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -247,6 +314,7 @@ exports[`test Developers should render the component for contributors with items
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
"debug": [Function],
|
"debug": [Function],
|
||||||
@ -329,10 +397,26 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emotion-3>* {
|
.emotion-3>* {
|
||||||
margin: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-4 {
|
.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;
|
position: relative;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
@ -362,9 +446,13 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
color: #f4f4f4;
|
color: #f4f4f4;
|
||||||
background-color: #bdbdbd;
|
background-color: #bdbdbd;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-5 {
|
.emotion-6 {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
@ -393,14 +481,20 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
<div
|
<div
|
||||||
class="emotion-2 MuiBox-root emotion-3"
|
class="emotion-2 MuiBox-root emotion-3"
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
aria-label="dmethvin"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
|
||||||
data-mui-internal-clone-element="true"
|
data-mui-internal-clone-element="true"
|
||||||
|
data-testid="dmethvin"
|
||||||
|
href="mailto:test@gmail.com?subject=undefined v1.0.0"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||||
data-testid="PersonIcon"
|
data-testid="PersonIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -410,14 +504,21 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</a>
|
||||||
aria-label="mgol"
|
<a
|
||||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||||
data-mui-internal-clone-element="true"
|
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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||||
data-testid="PersonIcon"
|
data-testid="PersonIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -427,6 +528,7 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>,
|
</body>,
|
||||||
@ -453,10 +555,26 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emotion-3>* {
|
.emotion-3>* {
|
||||||
margin: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-4 {
|
.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;
|
position: relative;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
@ -486,9 +604,13 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
color: #f4f4f4;
|
color: #f4f4f4;
|
||||||
background-color: #bdbdbd;
|
background-color: #bdbdbd;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emotion-5 {
|
.emotion-6 {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
@ -516,14 +638,20 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
<div
|
<div
|
||||||
class="emotion-2 MuiBox-root emotion-3"
|
class="emotion-2 MuiBox-root emotion-3"
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
aria-label="dmethvin"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
|
||||||
data-mui-internal-clone-element="true"
|
data-mui-internal-clone-element="true"
|
||||||
|
data-testid="dmethvin"
|
||||||
|
href="mailto:test@gmail.com?subject=undefined v1.0.0"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||||
data-testid="PersonIcon"
|
data-testid="PersonIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -533,14 +661,21 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</a>
|
||||||
aria-label="mgol"
|
<a
|
||||||
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4"
|
class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4"
|
||||||
data-mui-internal-clone-element="true"
|
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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5"
|
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6"
|
||||||
data-testid="PersonIcon"
|
data-testid="PersonIcon"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -550,6 +685,7 @@ exports[`test Developers should render the component for maintainers with items
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
"debug": [Function],
|
"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('sidebar.distribution.size')).toBeInTheDocument();
|
expect(getByText('sidebar.distribution.size')).toBeInTheDocument();
|
||||||
expect(getByText('7', { exact: false })).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', () => {
|
test('should render the component with license as string', () => {
|
||||||
|
@ -3,8 +3,8 @@ import React, { FC } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PackageMetaInterface } from '../../types/packageMeta';
|
import { PackageMetaInterface } from '../../types/packageMeta';
|
||||||
|
import { fileSizeSI, formatLicense } from '../../utils/utils';
|
||||||
import { DistChips, DistListItem, StyledText } from './styles';
|
import { DistChips, DistListItem, StyledText } from './styles';
|
||||||
import { fileSizeSI, formatLicense } from './utils';
|
|
||||||
|
|
||||||
const DistChip: FC<{ name: string; children?: React.ReactElement | string }> = ({
|
const DistChip: FC<{ name: string; children?: React.ReactElement | string }> = ({
|
||||||
name,
|
name,
|
||||||
@ -47,9 +47,7 @@ const Dist: FC<{ packageMeta: PackageMetaInterface }> = ({ packageMeta }) => {
|
|||||||
<DistChip name={t('sidebar.distribution.size')}>{fileSizeSI(dist.unpackedSize)}</DistChip>
|
<DistChip name={t('sidebar.distribution.size')}>{fileSizeSI(dist.unpackedSize)}</DistChip>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<DistChip name={t('sidebar.distribution.license')}>
|
<DistChip name={t('sidebar.distribution.license')}>{formatLicense(license)}</DistChip>
|
||||||
{formatLicense(license as string)}
|
|
||||||
</DistChip>
|
|
||||||
</DistListItem>
|
</DistListItem>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import FabMUI from '@mui/material/Fab';
|
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
@ -22,8 +21,3 @@ export const DistChips = styled(Chip)({
|
|||||||
textTransform: 'capitalize',
|
textTransform: 'capitalize',
|
||||||
marginTop: 5,
|
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}>
|
<Grid item={true} xs={6}>
|
||||||
<List subheader={<StyledText variant={'subtitle1'}>{title}</StyledText>}>
|
<List subheader={<StyledText variant={'subtitle1'}>{title}</StyledText>}>
|
||||||
<EngineListItem>
|
<EngineListItem>
|
||||||
<Avatar sx={{ bgcolor: 'transparent' }}>{element}</Avatar>
|
<Avatar sx={{ backgroundColor: 'transparent', marginLeft: 0, padding: 0 }}>
|
||||||
<Typography variant="subtitle2">{engineText}</Typography>
|
{element}
|
||||||
|
</Avatar>
|
||||||
|
<Typography sx={{ margin: 0, padding: '0 0 0 10px' }} variant="subtitle2">
|
||||||
|
{engineText}
|
||||||
|
</Typography>
|
||||||
</EngineListItem>
|
</EngineListItem>
|
||||||
</List>
|
</List>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -10,5 +10,5 @@ export const StyledText = styled(Typography)<{ theme?: Theme }>((props) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const EngineListItem = styled(ListItem)({
|
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 { Theme } from '../../Theme';
|
||||||
import { url } from '../../utils';
|
import { url } from '../../utils';
|
||||||
import { Link } from '../Link';
|
import LinkExternal from '../LinkExternal';
|
||||||
|
|
||||||
const StyledLink = styled(Link)<{ theme?: Theme }>(({ theme }) => ({
|
const StyledLink = styled(LinkExternal)<{ theme?: Theme }>(({ theme }) => ({
|
||||||
marginTop: theme?.spacing(1),
|
marginTop: theme?.spacing(2),
|
||||||
marginBottom: theme?.spacing(1),
|
marginBottom: theme?.spacing(2),
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
display: 'block',
|
display: 'block',
|
||||||
}));
|
}));
|
||||||
@ -32,7 +32,7 @@ const FundButton: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledLink external={true} to={fundingUrl} variant="button">
|
<StyledLink to={fundingUrl} variant="button">
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
fullWidth={true}
|
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}>
|
<RegistryInfoDialog onClose={onCloseDialog} open={isOpen} title={dialogTitle}>
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box sx={{ width: '100%' }}>
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
<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
|
||||||
? tabs.map((item, index) => {
|
? tabs.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
@ -68,7 +68,7 @@ const HeaderInfoDialog: React.FC<Props> = ({
|
|||||||
{tabPanels
|
{tabPanels
|
||||||
? tabPanels.map((item, index) => {
|
? tabPanels.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<TabPanel index={index} key={item.key} value={value}>
|
<TabPanel index={index} key={index} value={value}>
|
||||||
{item.element}
|
{item.element}
|
||||||
</TabPanel>
|
</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';
|
import React from 'react';
|
||||||
|
|
||||||
const icon = require('./commonjs.svg');
|
const icon = require('./commonjs.svg');
|
||||||
|
|
||||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
|
||||||
marginLeft: theme?.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function CommonJS() {
|
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';
|
import React from 'react';
|
||||||
|
|
||||||
const icon = require('./es6modules.svg');
|
const icon = require('./es6module.svg');
|
||||||
|
|
||||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
|
||||||
marginLeft: theme?.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function ES6Modules() {
|
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';
|
import React from 'react';
|
||||||
|
|
||||||
const icon = require('./git.svg');
|
const icon = require('./git.svg');
|
||||||
|
|
||||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
|
||||||
marginLeft: theme?.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function Git() {
|
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';
|
import React from 'react';
|
||||||
|
|
||||||
const icon = require('./nodejs.svg');
|
const icon = require('./nodejs.svg');
|
||||||
|
|
||||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
|
||||||
marginLeft: theme?.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function NodeJS() {
|
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';
|
import React from 'react';
|
||||||
|
|
||||||
const icon = require('./typescript.svg');
|
const icon = require('./typescript.svg');
|
||||||
|
|
||||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
|
||||||
marginLeft: theme?.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function TypeScript() {
|
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>
|
ref: React.Ref<SVGSVGElement>
|
||||||
) {
|
) {
|
||||||
return (
|
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" />
|
<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>
|
</SvgIcon>
|
||||||
);
|
);
|
||||||
|
@ -6,13 +6,17 @@ import {
|
|||||||
CommonJS,
|
CommonJS,
|
||||||
ES6Modules,
|
ES6Modules,
|
||||||
Earth,
|
Earth,
|
||||||
|
FileBinary,
|
||||||
Git,
|
Git,
|
||||||
Law,
|
Law,
|
||||||
License,
|
License,
|
||||||
NodeJS,
|
NodeJS,
|
||||||
|
Npm,
|
||||||
|
Pnpm,
|
||||||
Time,
|
Time,
|
||||||
TypeScript,
|
TypeScript,
|
||||||
Version,
|
Version,
|
||||||
|
Yarn,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -21,16 +25,28 @@ export default {
|
|||||||
|
|
||||||
export const Icons: any = () => (
|
export const Icons: any = () => (
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box sx={{ width: '100%' }}>
|
||||||
<Stack spacing={2}>
|
<Stack direction="row" spacing={2}>
|
||||||
|
<Npm />
|
||||||
|
<Pnpm />
|
||||||
|
<Yarn />
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
<NodeJS />
|
<NodeJS />
|
||||||
<Git />
|
<Git />
|
||||||
<Version />
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
<TypeScript />
|
<TypeScript />
|
||||||
<Time />
|
|
||||||
<License />
|
|
||||||
<Law />
|
|
||||||
<ES6Modules />
|
<ES6Modules />
|
||||||
<CommonJS />
|
<CommonJS />
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<Version />
|
||||||
|
<Time />
|
||||||
|
<FileBinary />
|
||||||
|
<Law />
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<License />
|
||||||
<Earth />
|
<Earth />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</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>) {
|
const Law = React.forwardRef(function Law(props: Props, ref: React.Ref<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<SvgIcon {...props} ref={ref}>
|
// eslint-disable-next-line verdaccio/jsx-spread
|
||||||
|
<SvgIcon viewBox="0 0 14 16" {...props} ref={ref}>
|
||||||
<path
|
<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"
|
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"
|
fillRule="evenodd"
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { Theme } from '@mui/material';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const icon = require('./npm.svg');
|
const icon = require('./npm.svg');
|
||||||
|
|
||||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
|
||||||
marginLeft: theme?.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function Npm() {
|
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';
|
import React from 'react';
|
||||||
|
|
||||||
const icon = require('./pnpm.svg');
|
const icon = require('./pnpm.svg');
|
||||||
|
|
||||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
|
||||||
marginLeft: theme?.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function Pnpm() {
|
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';
|
import React from 'react';
|
||||||
|
|
||||||
const icon = require('./yarn.svg');
|
const icon = require('./yarn.svg');
|
||||||
|
|
||||||
const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
|
|
||||||
marginLeft: theme?.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function Yarn() {
|
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 { useConfig } from '../../providers';
|
||||||
import { render, screen } from '../../test/test-react-testing-library';
|
import { render, screen } from '../../test/test-react-testing-library';
|
||||||
import Install from './Install';
|
import Install from './Install';
|
||||||
import { getGlobalInstall } from './InstallListItem';
|
import InstallListItem, { DependencyManager, getGlobalInstall } from './InstallListItem';
|
||||||
import data from './__partials__/data.json';
|
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();
|
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*/
|
/* eslint-disable react/jsx-no-bind*/
|
||||||
@ -22,6 +24,11 @@ describe('<Install />', () => {
|
|||||||
expect(screen.getByText('npm install foo@8.0.0')).toBeInTheDocument();
|
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', () => {
|
test('should have 3 children', () => {
|
||||||
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['yarn', 'pnpm', 'npm'];
|
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['yarn', 'pnpm', 'npm'];
|
||||||
const { getByTestId } = render(<ComponentToBeRendered />);
|
const { getByTestId } = render(<ComponentToBeRendered />);
|
||||||
@ -32,9 +39,7 @@ describe('<Install />', () => {
|
|||||||
|
|
||||||
test('should have the element NPM', () => {
|
test('should have the element NPM', () => {
|
||||||
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['npm'];
|
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['npm'];
|
||||||
|
|
||||||
render(<ComponentToBeRendered />);
|
render(<ComponentToBeRendered />);
|
||||||
|
|
||||||
expect(screen.getByText('sidebar.installation.title')).toBeTruthy();
|
expect(screen.getByText('sidebar.installation.title')).toBeTruthy();
|
||||||
expect(screen.queryByText('pnpm')).not.toBeInTheDocument();
|
expect(screen.queryByText('pnpm')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('yarn')).not.toBeInTheDocument();
|
expect(screen.queryByText('yarn')).not.toBeInTheDocument();
|
||||||
@ -59,18 +64,54 @@ describe('<Install />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getGlobalInstall', () => {
|
describe('<InstallListItem />', () => {
|
||||||
test('no global', () => {
|
test('renders correctly', () => {
|
||||||
expect(getGlobalInstall(false, 'foo', '1.0.0')).toEqual('1.0.0@foo');
|
render(
|
||||||
});
|
<InstallListItem
|
||||||
test('global', () => {
|
dependencyManager={DependencyManager.NPM}
|
||||||
expect(getGlobalInstall(true, 'foo', '1.0.0')).toEqual('-g 1.0.0@foo');
|
packageName={'foo'}
|
||||||
|
packageVersion={'8.0.0'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.queryByTestId('installListItem-npm')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('yarn no global', () => {
|
test('should not render if name is missing', () => {
|
||||||
expect(getGlobalInstall(false, 'foo', '1.0.0', true)).toEqual('1.0.0@foo');
|
render(
|
||||||
});
|
// @ts-ignore - testing invalid value
|
||||||
test('yarn global', () => {
|
<InstallListItem dependencyManager={'other'} packageName={'foo'} packageVersion={'8.0.0'} />
|
||||||
expect(getGlobalInstall(true, 'foo', '1.0.0', true)).toEqual('1.0.0@foo');
|
);
|
||||||
|
// 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 styled from '@emotion/styled';
|
||||||
import { Typography } from '@mui/material';
|
import { Typography } from '@mui/material';
|
||||||
import Grid from '@mui/material/Grid';
|
|
||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
import { useTheme } from '@mui/styles';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -18,6 +16,12 @@ const StyledText = styled(Typography)<{ theme?: Theme }>((props) => ({
|
|||||||
textTransform: 'capitalize',
|
textTransform: 'capitalize',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const Wrapper = styled('div')({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
});
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
packageMeta: PackageMetaInterface;
|
packageMeta: PackageMetaInterface;
|
||||||
packageName: string;
|
packageName: string;
|
||||||
@ -26,7 +30,6 @@ export type Props = {
|
|||||||
|
|
||||||
const Install: React.FC<Props> = ({ packageMeta, packageName, configOptions }) => {
|
const Install: React.FC<Props> = ({ packageMeta, packageName, configOptions }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
|
||||||
if (!packageMeta || !packageName) {
|
if (!packageMeta || !packageName) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -38,16 +41,14 @@ const Install: React.FC<Props> = ({ packageMeta, packageName, configOptions }) =
|
|||||||
|
|
||||||
return hasPkgManagers ? (
|
return hasPkgManagers ? (
|
||||||
<>
|
<>
|
||||||
<Grid
|
|
||||||
container={true}
|
|
||||||
justifyContent="flex-end"
|
|
||||||
sx={{ marginRight: theme.spacing(10), alingText: 'right' }}
|
|
||||||
>
|
|
||||||
<SettingsMenu packageName={packageName} />
|
|
||||||
</Grid>
|
|
||||||
<List
|
<List
|
||||||
data-testid={'installList'}
|
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 && (
|
{hasNpm && (
|
||||||
<InstallListItem
|
<InstallListItem
|
||||||
|
@ -22,9 +22,9 @@ const InstallListItemText = styled(ListItemText)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PackageMangerAvatar = styled(Avatar)({
|
const PackageMangerAvatar = styled(Avatar)({
|
||||||
borderRadius: '0px',
|
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
marginLeft: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export enum DependencyManager {
|
export enum DependencyManager {
|
||||||
@ -39,10 +39,10 @@ interface Interface {
|
|||||||
packageVersion?: string;
|
packageVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGlobalInstall(isGlobal, packageVersion, packageName, isYarn = false) {
|
export function getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, isYarn = false) {
|
||||||
const name = isGlobal
|
const name = isGlobal
|
||||||
? `${isYarn ? '' : '-g'} ${packageVersion ? `${packageName}@${packageVersion}` : packageName}`
|
? `${isYarn ? '' : '-g'} ${packageVersion && !isLatest ? `${packageName}@${packageVersion}` : packageName}`
|
||||||
: packageVersion
|
: packageVersion && !isLatest
|
||||||
? `${packageName}@${packageVersion}`
|
? `${packageName}@${packageVersion}`
|
||||||
: packageName;
|
: packageName;
|
||||||
|
|
||||||
@ -56,6 +56,7 @@ const InstallListItem: React.FC<Interface> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { localSettings } = useSettings();
|
const { localSettings } = useSettings();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const isLatest = localSettings[packageName]?.latest ?? false;
|
||||||
const isGlobal = localSettings[packageName]?.global ?? false;
|
const isGlobal = localSettings[packageName]?.global ?? false;
|
||||||
switch (dependencyManager) {
|
switch (dependencyManager) {
|
||||||
case DependencyManager.NPM:
|
case DependencyManager.NPM:
|
||||||
@ -67,9 +68,9 @@ const InstallListItem: React.FC<Interface> = ({
|
|||||||
<InstallListItemText
|
<InstallListItemText
|
||||||
primary={
|
primary={
|
||||||
<CopyToClipBoard
|
<CopyToClipBoard
|
||||||
dataTestId="instalNpm"
|
dataTestId="installNpm"
|
||||||
text={`npm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
|
text={`npm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`}
|
||||||
title={`npm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
|
title={`npm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -93,7 +94,7 @@ const InstallListItem: React.FC<Interface> = ({
|
|||||||
packageName,
|
packageName,
|
||||||
true
|
true
|
||||||
)}`
|
)}`
|
||||||
: `yarn add ${getGlobalInstall(isGlobal, packageVersion, packageName, true)}`
|
: `yarn add ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, true)}`
|
||||||
}
|
}
|
||||||
title={
|
title={
|
||||||
isGlobal
|
isGlobal
|
||||||
@ -103,7 +104,7 @@ const InstallListItem: React.FC<Interface> = ({
|
|||||||
packageName,
|
packageName,
|
||||||
true
|
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={
|
primary={
|
||||||
<CopyToClipBoard
|
<CopyToClipBoard
|
||||||
dataTestId="installPnpm"
|
dataTestId="installPnpm"
|
||||||
text={`pnpm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`}
|
text={`pnpm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`}
|
||||||
title={`pnpm install ${getGlobalInstall(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 React from 'react';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
|
||||||
type LinkRef = HTMLAnchorElement;
|
const CustomRouterLink = styled(RouterLink)`
|
||||||
|
|
||||||
export const CustomRouterLink = styled(RouterLink)`
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -13,23 +11,11 @@ export const CustomRouterLink = styled(RouterLink)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// TODO: improve any with custom types for a and RouterLink
|
const Link = React.forwardRef<HTMLAnchorElement, any>(function LinkFunction(
|
||||||
const Link = React.forwardRef<LinkRef, any>(function LinkFunction(
|
{ to, children, variant, className, onClick },
|
||||||
{ external, to, children, variant, className, onClick },
|
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
return external ? (
|
return (
|
||||||
<a
|
|
||||||
className={className}
|
|
||||||
href={to}
|
|
||||||
onClick={onClick}
|
|
||||||
ref={ref}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Typography variant={variant ?? 'caption'}>{children}</Typography>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<CustomRouterLink className={className} innerRef={ref} onClick={onClick} to={to}>
|
<CustomRouterLink className={className} innerRef={ref} onClick={onClick} to={to}>
|
||||||
<Typography variant={variant}>{children}</Typography>
|
<Typography variant={variant}>{children}</Typography>
|
||||||
</CustomRouterLink>
|
</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;
|
53
packages/ui-components/src/components/LinkExternal/__snapshots__/LinkExternal.test.tsx.snap
Normal file
53
packages/ui-components/src/components/LinkExternal/__snapshots__/LinkExternal.test.tsx.snap
Normal file
@ -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 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';
|
import Loading from './Loading';
|
||||||
|
|
||||||
describe('<Loading /> component', () => {
|
describe('<Loading /> component', () => {
|
||||||
test('should render the component in default state', () => {
|
test('should render the component in default state', async () => {
|
||||||
|
act(() => {
|
||||||
render(<Loading />);
|
render(<Loading />);
|
||||||
screen.debug();
|
});
|
||||||
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
@ -11,6 +11,7 @@ export const Wrapper = styled('div')({
|
|||||||
|
|
||||||
export const Badge = styled('div')<{ theme?: Theme }>(({ theme }) => ({
|
export const Badge = styled('div')<{ theme?: Theme }>(({ theme }) => ({
|
||||||
margin: '0 0 30px 0',
|
margin: '0 0 30px 0',
|
||||||
|
padding: 5,
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
boxShadow: '0 10px 20px 0 rgba(69, 58, 100, 0.2)',
|
boxShadow: '0 10px 20px 0 rgba(69, 58, 100, 0.2)',
|
||||||
background: theme?.palette.mode === 'dark' ? theme?.palette.black : '#f7f8f6',
|
background: theme?.palette.mode === 'dark' ? theme?.palette.black : '#f7f8f6',
|
||||||
|
@ -87,7 +87,7 @@ describe('<LoginDialog /> component', () => {
|
|||||||
fireEvent.change(userNameInput, { target: { value: 'xyz' } });
|
fireEvent.change(userNameInput, { target: { value: 'xyz' } });
|
||||||
|
|
||||||
const passwordInput = screen.getByPlaceholderText('form-placeholder.password');
|
const passwordInput = screen.getByPlaceholderText('form-placeholder.password');
|
||||||
expect(userNameInput).toBeInTheDocument();
|
expect(passwordInput).toBeInTheDocument();
|
||||||
fireEvent.focus(passwordInput);
|
fireEvent.focus(passwordInput);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
@ -21,7 +21,7 @@ const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
|
|||||||
const makeLogin = useCallback(
|
const makeLogin = useCallback(
|
||||||
async (username?: string, password?: string): Promise<LoginBody | void> => {
|
async (username?: string, password?: string): Promise<LoginBody | void> => {
|
||||||
// checks isEmpty
|
// checks isEmpty
|
||||||
if (isEmpty(username) || isEmpty(password)) {
|
if (!username || !password || isEmpty(username) || isEmpty(password)) {
|
||||||
dispatch.login.addError({
|
dispatch.login.addError({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
description: i18next.t('form-validation.username-or-password-cant-be-empty'),
|
description: i18next.t('form-validation.username-or-password-cant-be-empty'),
|
||||||
@ -29,6 +29,15 @@ const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
|
|||||||
return;
|
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 {
|
try {
|
||||||
dispatch.login.getUser({ username, password });
|
dispatch.login.getUser({ username, password });
|
||||||
// const response: LoginBody = await doLogin(username as string, password as string);
|
// 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,
|
right: theme.spacing() / 2,
|
||||||
top: theme.spacing() / 2,
|
top: theme.spacing() / 2,
|
||||||
color: theme.palette.grey[500],
|
color: theme.palette.grey[500],
|
||||||
|
zIndex: 99,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -48,7 +48,6 @@ const LoginDialogForm = memo(({ onSubmit, error }: Props) => {
|
|||||||
id="login--dialog-username"
|
id="login--dialog-username"
|
||||||
{...register('username', {
|
{...register('username', {
|
||||||
required: { value: true, message: t('form-validation.required-field') },
|
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')}
|
label={t('form.username')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
@ -65,7 +64,6 @@ const LoginDialogForm = memo(({ onSubmit, error }: Props) => {
|
|||||||
id="login--dialog-password"
|
id="login--dialog-password"
|
||||||
{...register('password', {
|
{...register('password', {
|
||||||
required: { value: true, message: t('form-validation.required-field') },
|
required: { value: true, message: t('form-validation.required-field') },
|
||||||
minLength: { value: 2, message: t('form-validation.required-min-length', { length: 2 }) },
|
|
||||||
})}
|
})}
|
||||||
data-testid="password"
|
data-testid="password"
|
||||||
label={t('form.password')}
|
label={t('form.password')}
|
||||||
|
@ -30,7 +30,7 @@ const LoginDialogFormError = memo(({ error }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<StyledSnackbarContent
|
<StyledSnackbarContent
|
||||||
message={
|
message={
|
||||||
<Box alignItems="center" display="flex">
|
<Box alignItems="center" data-testid="error" display="flex">
|
||||||
<StyledErrorIcon />
|
<StyledErrorIcon />
|
||||||
{error.description}
|
{error.description}
|
||||||
</Box>
|
</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 React from 'react';
|
||||||
|
|
||||||
import { render } from '../../test/test-react-testing-library';
|
import { render, screen } from '../../test/test-react-testing-library';
|
||||||
import Logo from './Logo';
|
import Logo from './Logo';
|
||||||
|
|
||||||
describe('<Logo /> component', () => {
|
describe('<Logo /> component', () => {
|
||||||
test('should render the component in default state', () => {
|
test('should render the component in default state', () => {
|
||||||
const { container } = render(<Logo />);
|
const { container } = render(<Logo />);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
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 styled from '@emotion/styled';
|
||||||
|
import { useTheme } from '@mui/styles';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Theme, useConfig } from '../../';
|
import { Theme, useConfig } from '../../';
|
||||||
@ -22,18 +23,33 @@ interface Props {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
isDefault?: boolean;
|
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 { configOptions } = useConfig();
|
||||||
|
const theme = useTheme();
|
||||||
if (!isDefault && configOptions?.logo) {
|
if (!isDefault && configOptions?.logo) {
|
||||||
|
const logoSrc =
|
||||||
|
theme?.palette.mode === 'dark' && configOptions.logoDark
|
||||||
|
? configOptions.logoDark
|
||||||
|
: configOptions.logo;
|
||||||
return (
|
return (
|
||||||
<ImageLogo className={className} onClick={onClick}>
|
<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>
|
</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;
|
export default Logo;
|
||||||
|
@ -17,5 +17,7 @@ exports[`<Logo /> component should render the component in default state 1`] = `
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="emotion-0 emotion-1"
|
class="emotion-0 emotion-1"
|
||||||
|
data-testid="default-logo"
|
||||||
|
title=""
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
@ -2,7 +2,13 @@ import React from 'react';
|
|||||||
import { MemoryRouter } from 'react-router';
|
import { MemoryRouter } from 'react-router';
|
||||||
|
|
||||||
import { store } from '../../';
|
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';
|
import Package from './Package';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,12 +17,6 @@ import Package from './Package';
|
|||||||
*/
|
*/
|
||||||
const dateOneMonthAgo = (): Date => new Date(1544377770747);
|
const dateOneMonthAgo = (): Date => new Date(1544377770747);
|
||||||
|
|
||||||
describe('<Package /> component', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should load the component', () => {
|
|
||||||
const props = {
|
const props = {
|
||||||
name: 'verdaccio',
|
name: 'verdaccio',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
@ -26,14 +26,27 @@ describe('<Package /> component', () => {
|
|||||||
author: {
|
author: {
|
||||||
name: 'Sam',
|
name: 'Sam',
|
||||||
},
|
},
|
||||||
keywords: ['verdaccio'],
|
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 wrapper = renderWithStore(
|
const wrapper = renderWithStore(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Package
|
<Package
|
||||||
author={props.author}
|
author={props.author}
|
||||||
description={props.description}
|
description={props.description}
|
||||||
|
dist={props.dist}
|
||||||
|
keywords={props.keywords}
|
||||||
license={props.license}
|
license={props.license}
|
||||||
name={props.name}
|
name={props.name}
|
||||||
time={props.time}
|
time={props.time}
|
||||||
@ -50,5 +63,25 @@ describe('<Package /> component', () => {
|
|||||||
expect(wrapper.getByText('MIT')).toBeInTheDocument();
|
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 BugReport from '@mui/icons-material/BugReport';
|
||||||
import DownloadIcon from '@mui/icons-material/CloudDownload';
|
import DownloadIcon from '@mui/icons-material/CloudDownload';
|
||||||
import HomeIcon from '@mui/icons-material/Home';
|
import HomeIcon from '@mui/icons-material/Home';
|
||||||
import { useTheme } from '@mui/material';
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
|
import List from '@mui/material/List';
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
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 { FileBinary, Law, Time, Version } from '../../components/Icons';
|
||||||
import { Author as PackageAuthor, PackageMetaInterface } from '../../types/packageMeta';
|
import { Author as PackageAuthor, PackageMetaInterface } from '../../types/packageMeta';
|
||||||
import { url, utils } from '../../utils';
|
import { Route, url, utils } from '../../utils';
|
||||||
import Tag from './Tag';
|
import KeywordListItems from '../Keywords/KeywordListItems';
|
||||||
import {
|
import {
|
||||||
Author,
|
Author,
|
||||||
Avatar,
|
Avatar,
|
||||||
@ -28,7 +28,6 @@ import {
|
|||||||
PackageListItemText,
|
PackageListItemText,
|
||||||
PackageTitle,
|
PackageTitle,
|
||||||
Published,
|
Published,
|
||||||
TagContainer,
|
|
||||||
Wrapper,
|
Wrapper,
|
||||||
WrapperLink,
|
WrapperLink,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
@ -47,7 +46,7 @@ export interface PackageInterface {
|
|||||||
time?: number | string;
|
time?: number | string;
|
||||||
author: PackageAuthor;
|
author: PackageAuthor;
|
||||||
description?: string;
|
description?: string;
|
||||||
keywords?: string[];
|
keywords?: PackageMetaInterface['latest']['keywords'];
|
||||||
license?: PackageMetaInterface['latest']['license'];
|
license?: PackageMetaInterface['latest']['license'];
|
||||||
homepage?: string;
|
homepage?: string;
|
||||||
bugs?: Bugs;
|
bugs?: Bugs;
|
||||||
@ -61,7 +60,7 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
description,
|
description,
|
||||||
dist,
|
dist,
|
||||||
homepage,
|
homepage,
|
||||||
keywords = [],
|
keywords,
|
||||||
license,
|
license,
|
||||||
name: packageName,
|
name: packageName,
|
||||||
time,
|
time,
|
||||||
@ -71,7 +70,6 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
const config = useSelector((state: RootState) => state.configuration.config);
|
const config = useSelector((state: RootState) => state.configuration.config);
|
||||||
const dispatch = useDispatch<Dispatch>();
|
const dispatch = useDispatch<Dispatch>();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
|
||||||
const isLoading = useSelector((state: RootState) => state?.loading?.models.download);
|
const isLoading = useSelector((state: RootState) => state?.loading?.models.download);
|
||||||
|
|
||||||
const handleDownload = useCallback(
|
const handleDownload = useCallback(
|
||||||
@ -94,21 +92,12 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
const renderAuthorInfo = (): React.ReactNode => {
|
const renderAuthorInfo = (): React.ReactNode => {
|
||||||
const name = utils.getAuthorName(authorName);
|
const name = utils.getAuthorName(authorName);
|
||||||
return (
|
return (
|
||||||
|
<OverviewItem>
|
||||||
<Author>
|
<Author>
|
||||||
<Avatar alt={name} src={authorAvatar} />
|
<Avatar alt={name} src={authorAvatar} />
|
||||||
<Details>
|
<Details>{name}</Details>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: theme?.fontWeight.semiBold,
|
|
||||||
color:
|
|
||||||
theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
</Details>
|
|
||||||
</Author>
|
</Author>
|
||||||
|
</OverviewItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,33 +121,34 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
time && (
|
time && (
|
||||||
<OverviewItem>
|
<OverviewItem>
|
||||||
<StyledTime />
|
<StyledTime />
|
||||||
<Published>{t('package.published-on', { time: utils.formatDate(time) })}</Published>
|
<Published title={utils.formatDate(time)}>
|
||||||
{utils.formatDateDistance(time)}
|
{t('package.published-on', { time: utils.formatDateDistance(time) })}
|
||||||
|
</Published>
|
||||||
</OverviewItem>
|
</OverviewItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderHomePageLink = (): React.ReactNode =>
|
const renderHomePageLink = (): React.ReactNode =>
|
||||||
homepage &&
|
homepage &&
|
||||||
url.isURL(homepage) && (
|
url.isURL(homepage) && (
|
||||||
<Link external={true} to={homepage}>
|
<LinkExternal to={homepage}>
|
||||||
<Tooltip aria-label={t('package.homepage')} title={t('package.visit-home-page')}>
|
<Tooltip aria-label={t('package.homepage')} title={t('package.visit-home-page')}>
|
||||||
<IconButton aria-label={t('package.homepage')} size="large">
|
<IconButton aria-label={t('package.homepage')} size="large">
|
||||||
<HomeIcon />
|
<HomeIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Link>
|
</LinkExternal>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderBugsLink = (): React.ReactNode =>
|
const renderBugsLink = (): React.ReactNode =>
|
||||||
bugs?.url &&
|
bugs?.url &&
|
||||||
url.isURL(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')}>
|
<Tooltip aria-label={t('package.bugs')} title={t('package.open-an-issue')}>
|
||||||
<IconButton aria-label={t('package.bugs')} size="large">
|
<IconButton aria-label={t('package.bugs')} size="large">
|
||||||
<BugReport />
|
<BugReport />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Link>
|
</LinkExternal>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderDownloadLink = (): React.ReactNode =>
|
const renderDownloadLink = (): React.ReactNode =>
|
||||||
@ -174,7 +164,11 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
aria-label={t('package.download', { what: t('package.the-tar-file') })}
|
aria-label={t('package.download', { what: t('package.the-tar-file') })}
|
||||||
title={t('package.tarball')}
|
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 ? (
|
{isLoading ? (
|
||||||
<CircularProgress size={13}>
|
<CircularProgress size={13}>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
@ -191,7 +185,7 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
return (
|
return (
|
||||||
<Grid container={true} item={true} xs={12}>
|
<Grid container={true} item={true} xs={12}>
|
||||||
<Grid item={true} xs={11}>
|
<Grid item={true} xs={11}>
|
||||||
<WrapperLink to={`/-/web/detail/${packageName}`}>
|
<WrapperLink to={`${Route.DETAIL}${packageName}`}>
|
||||||
<PackageTitle className="package-title" data-testid="package-title">
|
<PackageTitle className="package-title" data-testid="package-title">
|
||||||
{packageName}
|
{packageName}
|
||||||
</PackageTitle>
|
</PackageTitle>
|
||||||
@ -213,15 +207,7 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSecondaryComponent = (): React.ReactNode => {
|
const renderSecondaryComponent = (): React.ReactNode => <Description>{description}</Description>;
|
||||||
const tags = keywords.sort().map((keyword, index) => <Tag key={index}>{keyword}</Tag>);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Description>{description}</Description>
|
|
||||||
{tags.length > 0 && <TagContainer>{tags}</TagContainer>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPackageListItemText = (): React.ReactNode => (
|
const renderPackageListItemText = (): React.ReactNode => (
|
||||||
<PackageListItemText
|
<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 (
|
return (
|
||||||
<Wrapper className={'package'} data-testid="package-item-list">
|
<Wrapper className={'package'} data-testid="package-item-list">
|
||||||
<ListItem alignItems={'flex-start'}>{renderPackageListItemText()}</ListItem>
|
<ListItem alignItems={'flex-start'} sx={{ mb: 0 }}>
|
||||||
<ListItem alignItems={'flex-start'}>
|
{renderPackageListItemText()}
|
||||||
|
</ListItem>
|
||||||
|
{keywords && keywords?.length > 0 ? renderKeywords() : null}
|
||||||
|
<ListItem alignItems={'flex-start'} sx={{ mt: 0 }}>
|
||||||
{renderAuthorInfo()}
|
{renderAuthorInfo()}
|
||||||
{renderVersionInfo()}
|
{renderVersionInfo()}
|
||||||
{renderPublishedInfo()}
|
{renderPublishedInfo()}
|
||||||
@ -248,8 +245,8 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
export default Package;
|
export default Package;
|
||||||
|
|
||||||
const iconStyle = ({ theme }: { theme: Theme }) => css`
|
const iconStyle = ({ theme }: { theme: Theme }) => css`
|
||||||
margin: 2px 10px 0 0;
|
margin: 0 10px 0 0;
|
||||||
fill: ${theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white};
|
fill: ${theme?.palette.mode === 'light' ? theme?.palette.greyDark : theme?.palette.white};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledVersion = styled(Version)`
|
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 }) => ({
|
export const OverviewItem = styled('span')<{ theme?: Theme }>(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
margin: '0 0 0 16px',
|
margin: '0 20px 0 0',
|
||||||
color: theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white,
|
color: theme?.palette.mode === 'light' ? theme?.palette.greyDark2 : theme?.palette.white,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
[`@media (max-width: ${theme?.breakPoints.medium}px)`]: {
|
[`@media (max-width: ${theme?.breakPoints.medium}px)`]: {
|
||||||
':nth-of-type(3)': {
|
':nth-of-type(3)': {
|
||||||
@ -26,10 +26,9 @@ export const OverviewItem = styled('span')<{ theme?: Theme }>(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const Published = styled('span')<{ theme?: Theme }>(({ theme }) => ({
|
export const Published = styled('span')({
|
||||||
color: theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white,
|
|
||||||
margin: '0 5px 0 0',
|
margin: '0 5px 0 0',
|
||||||
}));
|
});
|
||||||
|
|
||||||
export const Details = styled('span')({
|
export const Details = styled('span')({
|
||||||
marginLeft: '5px',
|
marginLeft: '5px',
|
||||||
@ -59,6 +58,7 @@ export const PackageTitle = styled('span')<{ theme?: Theme }>(({ theme }) => ({
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
color: theme?.palette.mode == 'dark' ? theme?.palette.dodgerBlue : theme?.palette.eclipse,
|
color: theme?.palette.mode == 'dark' ? theme?.palette.dodgerBlue : theme?.palette.eclipse,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'none',
|
||||||
[`@media (max-width: ${theme?.breakPoints.small}px)`]: {
|
[`@media (max-width: ${theme?.breakPoints.small}px)`]: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
@ -70,9 +70,9 @@ export const GridRightAligned = styled(Grid)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const Wrapper = styled(List)<{ theme?: Theme }>(({ theme }) => ({
|
export const Wrapper = styled(List)<{ theme?: Theme }>(({ theme }) => ({
|
||||||
':hover': {
|
'&:hover': {
|
||||||
backgroundColor:
|
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)({
|
export const PackageListItemText = styled(ListItemText)({
|
||||||
paddingRight: 0,
|
paddingRight: 0,
|
||||||
});
|
});
|
||||||
|
@ -40,6 +40,7 @@ describe('<PackageList /> component', () => {
|
|||||||
name: 'xyz',
|
name: 'xyz',
|
||||||
version: '1.1.0',
|
version: '1.1.0',
|
||||||
description: 'xyz description',
|
description: 'xyz description',
|
||||||
|
keywords: ['hello', 'mars'],
|
||||||
author: { name: 'Martin', avatar: 'test avatar' },
|
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 New Issue
Block a user