From 10dd81f4734d98d40a6cf9a85f61eae65312acdf Mon Sep 17 00:00:00 2001
From: Marc Bernard <59966492+mbtools@users.noreply.github.com>
Date: Sun, 7 Jul 2024 14:12:24 +0200
Subject: [PATCH] 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
---
.changeset/dirty-dolphins-try.md | 10 +
packages/config/src/conf/default.yaml | 7 +
packages/config/src/conf/docker.yaml | 7 +
packages/core/types/src/configuration.ts | 2 +
.../src/middlewares/web/render-web.ts | 62 +-
.../src/middlewares/web/utils/renderHTML.ts | 24 +-
.../middleware/test/config/file-logo.yaml | 1 +
packages/middleware/test/render.spec.ts | 7 +
.../plugins/ui-theme/src/App/App.test.tsx | 14 +-
.../plugins/ui-theme/src/i18n/crowdin/ui.json | 15 +-
packages/ui-components/.eslintrc | 2 +-
.../jest/api/storybook-readme.js | 3 +
.../jest/api/storybook-readme.txt | 2 -
.../ui-components/jest/server-handlers.ts | 21 +-
.../src/AppTest/AppRoute.test.tsx | 65 ++
.../src/Theme/ThemeProvider.test.tsx | 27 +
packages/ui-components/src/Theme/theme.ts | 6 +-
.../components/ActionBar/ActionBar.test.tsx | 51 +-
.../src/components/ActionBar/ActionBar.tsx | 2 +-
.../components/ActionBar/ActionBarAction.tsx | 13 +-
.../src/components/Author/Author.test.tsx | 8 +-
.../src/components/Author/Author.tsx | 44 +-
.../Author/__snapshots__/Author.test.tsx.snap | 88 ++-
.../CopyClipboard/CopyToClipBoard.test.tsx | 23 +
.../src/components/CopyClipboard/utils.ts | 15 +-
.../components/Dependencies/Dependencies.tsx | 18 +-
.../Dependencies/DependencyBlock.tsx | 3 +-
.../components/Developers/DeveloperType.tsx | 4 +
.../components/Developers/Developers.test.tsx | 47 +-
.../src/components/Developers/Developers.tsx | 26 +-
.../src/components/Developers/Title.tsx | 6 +-
.../__snapshots__/Developers.test.tsx.snap | 376 ++++++++----
.../src/components/Developers/index.ts | 3 +-
.../src/components/Developers/styles.ts | 31 -
.../src/components/Distribution/Dist.test.tsx | 2 +-
.../src/components/Distribution/Dist.tsx | 6 +-
.../src/components/Distribution/styles.ts | 6 -
.../src/components/Distribution/utils.ts | 32 -
.../components/Distribution/utilts.spec.ts | 17 -
.../src/components/Engines/Engines.tsx | 8 +-
.../src/components/Engines/styles.ts | 2 +-
.../ErrorBoundary/ErrorBoundary.test.tsx | 39 ++
.../src/components/FundButton/FundButton.tsx | 10 +-
.../HeaderInfoDialog.test.tsx | 51 ++
.../HeaderInfoDialog/HeaderInfoDialog.tsx | 4 +-
.../src/components/Heading/Heading.test.tsx | 20 +
.../components/Icons/DevsIcons/CommonJS.tsx | 8 +-
.../components/Icons/DevsIcons/ES6Module.tsx | 10 +-
.../src/components/Icons/DevsIcons/Git.tsx | 8 +-
.../src/components/Icons/DevsIcons/NodeJS.tsx | 8 +-
.../components/Icons/DevsIcons/TypeScript.tsx | 8 +-
.../{es6modules.svg => es6module.svg} | 0
.../src/components/Icons/FileBinary.tsx | 3 +-
.../src/components/Icons/Icons.stories.tsx | 26 +-
.../src/components/Icons/Icons.test.tsx | 70 +++
.../src/components/Icons/Law.tsx | 3 +-
.../src/components/Icons/Managers/Npm.tsx | 8 +-
.../src/components/Icons/Managers/Pnpm.tsx | 8 +-
.../src/components/Icons/Managers/Yarn.tsx | 8 +-
.../src/components/Install/Install.test.tsx | 73 ++-
.../src/components/Install/Install.tsx | 23 +-
.../components/Install/InstallListItem.tsx | 23 +-
.../components/Keywords/KeywordListItems.tsx | 22 +
.../components/Keywords/Keywords.stories.tsx | 19 +
.../src/components/Keywords/Keywords.test.tsx | 57 ++
.../src/components/Keywords/Keywords.tsx | 35 ++
.../src/components/Keywords/index.ts | 1 +
.../src/components/Link/Link.test.tsx | 25 +
.../src/components/Link/Link.tsx | 22 +-
.../Link/__snapshots__/Link.test.tsx.snap | 63 ++
.../src/components/Link/index.ts | 2 +-
.../LinkExternal/LinkExternal.test.tsx | 18 +
.../components/LinkExternal/LinkExternal.tsx | 22 +
.../__snapshots__/LinkExternal.test.tsx.snap | 53 ++
.../src/components/LinkExternal/index.ts | 1 +
.../src/components/Loading/Loading.test.tsx | 13 +-
.../src/components/Loading/styles.ts | 1 +
.../LoginDialog/LoginDialog.test.tsx | 2 +-
.../components/LoginDialog/LoginDialog.tsx | 11 +-
.../LoginDialog/LoginDialogCloseButton.tsx | 1 +
.../LoginDialog/LoginDialogForm.tsx | 2 -
.../LoginDialog/LoginDialogFormError.tsx | 2 +-
.../src/components/Logo/Logo.stories.tsx | 16 +
.../src/components/Logo/Logo.test.tsx | 9 +-
.../src/components/Logo/Logo.tsx | 22 +-
.../Logo/__snapshots__/Logo.test.tsx.snap | 2 +
.../src/components/Package/Package.test.tsx | 61 +-
.../src/components/Package/Package.tsx | 85 ++-
.../src/components/Package/Tag/Tag.test.tsx | 15 -
.../src/components/Package/Tag/Tag.tsx | 11 -
.../Tag/__snapshots__/Tag.test.tsx.snap | 21 -
.../src/components/Package/Tag/index.ts | 1 -
.../src/components/Package/Tag/styles.ts | 11 -
.../src/components/Package/styles.ts | 24 +-
.../PackageList/Packagelist.test.tsx | 1 +
.../src/components/Person/Person.test.tsx | 50 ++
.../src/components/Person/Person.tsx | 45 ++
.../src/components/Person/PersonTooltip.tsx | 26 +
.../src/components/Person/index.ts | 1 +
.../src/components/Person/utils.ts | 20 +
.../src/components/RawViewer/RawViewer.tsx | 12 +-
.../src/components/Readme/Readme.spec.tsx | 17 +
.../src/components/Readme/Readme.tsx | 15 +-
.../Readme/__snapshots__/Readme.spec.tsx.snap | 6 +-
.../src/components/Readme/utils.ts | 7 +-
.../components/RegistryInfoDialog/styles.ts | 9 +-
.../src/components/Repository/Repository.tsx | 23 +-
.../components/Search/AutoComplete/styles.ts | 7 +
.../components/Search/AutoComplete/styles.tsx | 42 --
.../src/components/Search/Search.test.tsx | 45 +-
.../src/components/Search/Search.tsx | 6 +-
.../src/components/Search/SearchItem.tsx | 27 +-
.../src/components/Search/utils.ts | 17 +
.../SettingsMenu/SettingsMenu.test.tsx | 41 ++
.../components/SettingsMenu/SettingsMenu.tsx | 64 +-
.../components/SideBarTitle/SideBarTitle.tsx | 22 +-
.../src/components/UpLinks/UpLinks.test.tsx | 7 +
.../src/components/UpLinks/UpLinks.tsx | 53 +-
.../src/components/UpLinks/UplinkLink.tsx | 20 +
.../__snapshots__/UpLinks.test.tsx.snap | 572 ++++++++++++------
.../src/components/UpLinks/styles.ts | 1 +
.../src/components/Versions/HistoryList.tsx | 13 +-
.../src/components/Versions/TagList.tsx | 7 +-
.../src/components/Versions/Versions.test.tsx | 2 +
.../src/components/Versions/Versions.tsx | 89 ++-
packages/ui-components/src/index.ts | 5 +-
.../AppConfigurationProvider.tsx | 1 +
.../PersistenceSettingProvider.tsx | 2 +
.../VersionProvider/VersionProvider.test.tsx | 6 +-
.../src/sections/Detail/Detail.test.tsx | 9 +-
.../src/sections/Detail/Detail.tsx | 15 +-
.../src/sections/Detail/Tabs.tsx | 7 +-
.../Detail/__snapshots__/Detail.test.tsx.snap | 2 +-
.../src/sections/Footer/Footer.tsx | 19 +-
.../src/sections/Footer/styles.ts | 4 +
.../src/sections/Header/Header.test.tsx | 22 +-
.../src/sections/Header/HeaderLeft.tsx | 30 +-
.../src/sections/Header/HeaderMenu.tsx | 2 +-
.../src/sections/Header/HeaderRight.tsx | 17 +-
.../sections/Header/HeaderSettingsDialog.tsx | 2 +-
.../RegistryInfoContent.test.tsx | 2 +-
.../RegistryInfoDialog/RegistryInfoDialog.tsx | 31 -
.../Header/RegistryInfoDialog/index.ts | 1 -
.../Header/RegistryInfoDialog/styles.ts | 21 -
.../Header/RegistryInfoDialog/types.ts | 8 -
.../src/sections/Header/styles.ts | 20 +-
.../src/sections/Home/Home.test.tsx | 16 +-
.../src/sections/SideBar/Sidebar.test.tsx | 33 +-
.../src/sections/SideBar/Sidebar.tsx | 14 +-
packages/ui-components/src/store/api.test.ts | 16 +
.../src/store/models/configuration.ts | 3 +-
.../ui-components/src/store/models/login.ts | 22 +-
.../src/store/models/manifest.ts | 11 +-
.../src/store/models/packages.ts | 9 +-
.../ui-components/src/store/models/routes.ts | 10 +
.../ui-components/src/store/models/search.ts | 6 +-
.../ui-components/src/store/models/utils.ts | 3 +
.../ui-components/src/types/packageMeta.ts | 12 +-
packages/ui-components/src/utils/routes.ts | 1 +
.../ui-components/src/utils/utils.test.ts | 34 ++
packages/ui-components/src/utils/utils.ts | 25 +-
packages/ui-components/tsconfig.json | 2 +-
162 files changed, 2657 insertions(+), 1220 deletions(-)
create mode 100644 .changeset/dirty-dolphins-try.md
create mode 100644 packages/ui-components/jest/api/storybook-readme.js
delete mode 100644 packages/ui-components/jest/api/storybook-readme.txt
create mode 100644 packages/ui-components/src/AppTest/AppRoute.test.tsx
create mode 100644 packages/ui-components/src/Theme/ThemeProvider.test.tsx
create mode 100644 packages/ui-components/src/components/CopyClipboard/CopyToClipBoard.test.tsx
create mode 100644 packages/ui-components/src/components/Developers/DeveloperType.tsx
delete mode 100644 packages/ui-components/src/components/Developers/styles.ts
delete mode 100644 packages/ui-components/src/components/Distribution/utils.ts
delete mode 100644 packages/ui-components/src/components/Distribution/utilts.spec.ts
create mode 100644 packages/ui-components/src/components/ErrorBoundary/ErrorBoundary.test.tsx
create mode 100644 packages/ui-components/src/components/HeaderInfoDialog/HeaderInfoDialog.test.tsx
create mode 100644 packages/ui-components/src/components/Heading/Heading.test.tsx
rename packages/ui-components/src/components/Icons/DevsIcons/{es6modules.svg => es6module.svg} (100%)
create mode 100644 packages/ui-components/src/components/Icons/Icons.test.tsx
create mode 100644 packages/ui-components/src/components/Keywords/KeywordListItems.tsx
create mode 100644 packages/ui-components/src/components/Keywords/Keywords.stories.tsx
create mode 100644 packages/ui-components/src/components/Keywords/Keywords.test.tsx
create mode 100644 packages/ui-components/src/components/Keywords/Keywords.tsx
create mode 100644 packages/ui-components/src/components/Keywords/index.ts
create mode 100644 packages/ui-components/src/components/Link/Link.test.tsx
create mode 100644 packages/ui-components/src/components/Link/__snapshots__/Link.test.tsx.snap
create mode 100644 packages/ui-components/src/components/LinkExternal/LinkExternal.test.tsx
create mode 100644 packages/ui-components/src/components/LinkExternal/LinkExternal.tsx
create mode 100644 packages/ui-components/src/components/LinkExternal/__snapshots__/LinkExternal.test.tsx.snap
create mode 100644 packages/ui-components/src/components/LinkExternal/index.ts
create mode 100644 packages/ui-components/src/components/Logo/Logo.stories.tsx
delete mode 100644 packages/ui-components/src/components/Package/Tag/Tag.test.tsx
delete mode 100644 packages/ui-components/src/components/Package/Tag/Tag.tsx
delete mode 100644 packages/ui-components/src/components/Package/Tag/__snapshots__/Tag.test.tsx.snap
delete mode 100644 packages/ui-components/src/components/Package/Tag/index.ts
delete mode 100644 packages/ui-components/src/components/Package/Tag/styles.ts
create mode 100644 packages/ui-components/src/components/Person/Person.test.tsx
create mode 100644 packages/ui-components/src/components/Person/Person.tsx
create mode 100644 packages/ui-components/src/components/Person/PersonTooltip.tsx
create mode 100644 packages/ui-components/src/components/Person/index.ts
create mode 100644 packages/ui-components/src/components/Person/utils.ts
create mode 100644 packages/ui-components/src/components/Search/AutoComplete/styles.ts
delete mode 100644 packages/ui-components/src/components/Search/AutoComplete/styles.tsx
create mode 100644 packages/ui-components/src/components/Search/utils.ts
create mode 100644 packages/ui-components/src/components/SettingsMenu/SettingsMenu.test.tsx
create mode 100644 packages/ui-components/src/components/UpLinks/UplinkLink.tsx
delete mode 100644 packages/ui-components/src/sections/Header/RegistryInfoDialog/RegistryInfoDialog.tsx
delete mode 100644 packages/ui-components/src/sections/Header/RegistryInfoDialog/index.ts
delete mode 100644 packages/ui-components/src/sections/Header/RegistryInfoDialog/styles.ts
delete mode 100644 packages/ui-components/src/sections/Header/RegistryInfoDialog/types.ts
create mode 100644 packages/ui-components/src/store/models/routes.ts
create mode 100644 packages/ui-components/src/store/models/utils.ts
diff --git a/.changeset/dirty-dolphins-try.md b/.changeset/dirty-dolphins-try.md
new file mode 100644
index 000000000..4cfa53b29
--- /dev/null
+++ b/.changeset/dirty-dolphins-try.md
@@ -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
diff --git a/packages/config/src/conf/default.yaml b/packages/config/src/conf/default.yaml
index 20789bfa4..638828431 100644
--- a/packages/config/src/conf/default.yaml
+++ b/packages/config/src/conf/default.yaml
@@ -18,6 +18,12 @@ storage: ./storage
# https://verdaccio.org/docs/webui
web:
title: Verdaccio
+ # custom colors for header background and font
+ # primaryColor: "#4b5e40"
+ # custom logos and favicon
+ # logo: ./path/to/logo.png
+ # logoDark: ./path/to/logoDark.png
+ # favicon: ./path/to/favicon.ico
# comment out to disable gravatar support
# gravatar: false
# by default packages are ordercer ascendant (asc|desc)
@@ -35,6 +41,7 @@ web:
# showSearch: true
# showRaw: true
# showDownloadTarball: true
+ # showUplinks: true
# HTML tags injected after manifest
# scriptsBodyAfter:
# - ''
diff --git a/packages/config/src/conf/docker.yaml b/packages/config/src/conf/docker.yaml
index dafe5b9e4..ddb62b516 100644
--- a/packages/config/src/conf/docker.yaml
+++ b/packages/config/src/conf/docker.yaml
@@ -21,6 +21,12 @@ plugins: /verdaccio/plugins
# https://verdaccio.org/docs/webui
web:
title: Verdaccio
+ # custom colors for header background and font
+ # primaryColor: "#4b5e40"
+ # custom logos and favicon
+ # logo: ./path/to/logo.png
+ # logoDark: ./path/to/logoDark.png
+ # favicon: ./path/to/favicon.ico
# Comment out to disable gravatar support
# gravatar: false
# By default packages are ordered ascendant (asc|desc)
@@ -38,6 +44,7 @@ web:
# showSearch: true
# showRaw: true
# showDownloadTarball: true
+ # showUplinks: true
# HTML tags injected after manifest
# scriptsBodyAfter:
# - ''
diff --git a/packages/core/types/src/configuration.ts b/packages/core/types/src/configuration.ts
index 7ee8ba8a6..3f303e521 100644
--- a/packages/core/types/src/configuration.ts
+++ b/packages/core/types/src/configuration.ts
@@ -83,6 +83,7 @@ export type PackageManagers = 'pnpm' | 'yarn' | 'npm';
export type CommonWebConf = {
title?: string;
logo?: string;
+ logoDark?: string;
favicon?: string;
gravatar?: boolean;
sort_packages?: string;
@@ -98,6 +99,7 @@ export type CommonWebConf = {
showFooter?: boolean;
showThemeSwitch?: boolean;
showDownloadTarball?: boolean;
+ showUplinks?: boolean;
hideDeprecatedVersions?: boolean;
primaryColor: string;
showRaw?: boolean;
diff --git a/packages/middleware/src/middlewares/web/render-web.ts b/packages/middleware/src/middlewares/web/render-web.ts
index 1bc76d91f..3064a42d3 100644
--- a/packages/middleware/src/middlewares/web/render-web.ts
+++ b/packages/middleware/src/middlewares/web/render-web.ts
@@ -42,34 +42,46 @@ export function renderWebMiddleware(config, tokenMiddleware, pluginOptions) {
res.sendFile(file, sendFileCallback(next));
});
- // check the origin of the logo
- if (config?.web?.logo && !isURLhasValidProtocol(config?.web?.logo)) {
- // URI related to a local file
- const absoluteLocalFile = path.posix.resolve(config.web.logo);
- debug('serve local logo %s', absoluteLocalFile);
- try {
- // TODO: replace existsSync by async alternative
- if (
- fs.existsSync(absoluteLocalFile) &&
- typeof fs.accessSync(absoluteLocalFile, fs.constants.R_OK) === 'undefined'
- ) {
- // Note: `path.join` will break on Windows, because it transforms `/` to `\`
- // Use POSIX version `path.posix.join` instead.
- config.web.logo = path.posix.join('/-/static/', path.basename(config.web.logo));
- router.get(config.web.logo, function (_req, res, next) {
- // @ts-ignore
- debug('serve custom logo web:%s - local:%s', config.web.logo, absoluteLocalFile);
- res.sendFile(absoluteLocalFile, sendFileCallback(next));
- });
- debug('enabled custom logo %s', config.web.logo);
- } else {
- config.web.logo = undefined;
+ function renderLogo(logo: string | undefined): string | undefined {
+ // check the origin of the logo
+ if (logo && !isURLhasValidProtocol(logo)) {
+ // URI related to a local file
+ const absoluteLocalFile = path.posix.resolve(logo);
+ debug('serve local logo %s', absoluteLocalFile);
+ try {
+ // TODO: replace existsSync by async alternative
+ if (
+ fs.existsSync(absoluteLocalFile) &&
+ typeof fs.accessSync(absoluteLocalFile, fs.constants.R_OK) === 'undefined'
+ ) {
+ // Note: `path.join` will break on Windows, because it transforms `/` to `\`
+ // Use POSIX version `path.posix.join` instead.
+ logo = path.posix.join('/-/static/', path.basename(logo));
+ router.get(logo, function (_req, res, next) {
+ // @ts-ignore
+ debug('serve custom logo web:%s - local:%s', logo, absoluteLocalFile);
+ res.sendFile(absoluteLocalFile, sendFileCallback(next));
+ });
+ debug('enabled custom logo %s', logo);
+ } else {
+ logo = undefined;
+ debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
+ }
+ } catch {
+ logo = undefined;
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
}
- } catch {
- config.web.logo = undefined;
- debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
}
+ return logo;
+ }
+
+ const logo = renderLogo(config?.web?.logo);
+ if (config?.web?.logo) {
+ config.web.logo = logo;
+ }
+ const logoDark = renderLogo(config?.web?.logoDark);
+ if (config?.web?.logoDark) {
+ config.web.logoDark = logoDark;
}
router.get('/-/web/:section/*', function (req, res) {
diff --git a/packages/middleware/src/middlewares/web/utils/renderHTML.ts b/packages/middleware/src/middlewares/web/utils/renderHTML.ts
index a36b0f89a..65f1f1077 100644
--- a/packages/middleware/src/middlewares/web/utils/renderHTML.ts
+++ b/packages/middleware/src/middlewares/web/utils/renderHTML.ts
@@ -26,16 +26,20 @@ const defaultManifestFiles: Manifest = {
css: [],
};
-export function resolveLogo(config: ConfigYaml, requestOptions: RequestOptions) {
- if (typeof config?.web?.logo !== 'string') {
+export function resolveLogo(
+ logo: string | undefined,
+ url_prefix: string | undefined,
+ requestOptions: RequestOptions
+) {
+ if (typeof logo !== 'string') {
return '';
}
- const isLocalFile = config?.web?.logo && !isURLhasValidProtocol(config?.web?.logo);
+ const isLocalFile = logo && !isURLhasValidProtocol(logo);
if (isLocalFile) {
- return `${getPublicUrl(config?.url_prefix, requestOptions)}-/static/${path.basename(config?.web?.logo)}`;
- } else if (isURLhasValidProtocol(config?.web?.logo)) {
- return config?.web?.logo;
+ return `${getPublicUrl(url_prefix, requestOptions)}-/static/${path.basename(logo)}`;
+ } else if (isURLhasValidProtocol(logo)) {
+ return logo;
} else {
return '';
}
@@ -61,7 +65,8 @@ export default function renderHTML(
const title = config?.web?.title ?? WEB_TITLE;
const login = hasLogin(config);
const scope = config?.web?.scope ?? '';
- const logo = resolveLogo(config, requestOptions);
+ const logo = resolveLogo(config?.web?.logo, config?.url_prefix, requestOptions);
+ const logoDark = resolveLogo(config?.web?.logoDark, config?.url_prefix, requestOptions);
const pkgManagers = config?.web?.pkgManagers ?? ['yarn', 'pnpm', 'npm'];
const version = res.locals.app_version ?? '';
const flags = {
@@ -81,6 +86,8 @@ export default function renderHTML(
showFooter,
showSearch,
showDownloadTarball,
+ showRaw,
+ showUplinks,
} = Object.assign(
{},
{
@@ -97,6 +104,8 @@ export default function renderHTML(
showFooter,
showSearch,
showDownloadTarball,
+ showRaw,
+ showUplinks,
darkMode,
url_prefix,
basename,
@@ -104,6 +113,7 @@ export default function renderHTML(
primaryColor,
version,
logo,
+ logoDark,
flags,
login,
pkgManagers,
diff --git a/packages/middleware/test/config/file-logo.yaml b/packages/middleware/test/config/file-logo.yaml
index 4a4b4adbf..468ad357a 100644
--- a/packages/middleware/test/config/file-logo.yaml
+++ b/packages/middleware/test/config/file-logo.yaml
@@ -14,6 +14,7 @@ web:
showRaw: true
primary_color: '#ffffff'
logo: './test/config/dark-logo.png'
+ logoDark: './test/config/dark-logo.png'
html_cache: false
url_prefix: /prefix
diff --git a/packages/middleware/test/render.spec.ts b/packages/middleware/test/render.spec.ts
index 4cf40bec7..810f3adb8 100644
--- a/packages/middleware/test/render.spec.ts
+++ b/packages/middleware/test/render.spec.ts
@@ -79,6 +79,13 @@ describe('test web server', () => {
return loadLogo('file-logo.yaml', '/-/static/dark-logo.png');
});
+ test('should render dark logo as file', async () => {
+ const {
+ window: { __VERDACCIO_BASENAME_UI_OPTIONS },
+ } = await render('file-logo.yaml');
+ expect(__VERDACCIO_BASENAME_UI_OPTIONS.logoDark).toMatch('/prefix/-/static/dark-logo.png');
+ });
+
test('should not render logo as absolute file is wrong', async () => {
const {
window: { __VERDACCIO_BASENAME_UI_OPTIONS },
diff --git a/packages/plugins/ui-theme/src/App/App.test.tsx b/packages/plugins/ui-theme/src/App/App.test.tsx
index fe8c8ac05..6bc80e351 100644
--- a/packages/plugins/ui-theme/src/App/App.test.tsx
+++ b/packages/plugins/ui-theme/src/App/App.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { renderWithStore, screen } from 'verdaccio-ui/utils/test-react-testing-library';
+import { act, renderWithStore, screen } from 'verdaccio-ui/utils/test-react-testing-library';
import { store } from '@verdaccio/ui-components';
@@ -13,17 +13,21 @@ jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600);
/* eslint-disable react/jsx-no-bind*/
describe(' ', () => {
describe('footer', () => {
- test('should display the Header component', () => {
- renderWithStore( , store);
+ test('should display the Header component', async () => {
+ await act(() => {
+ renderWithStore( , store);
+ });
expect(screen.getByTestId('footer')).toBeInTheDocument();
});
- test('should not display the Header component', () => {
+ test('should not display the Header component', async () => {
// @ts-ignore
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
showFooter: false,
};
- renderWithStore( , store);
+ await act(() => {
+ renderWithStore( , store);
+ });
expect(screen.queryByTestId('footer')).toBeFalsy();
});
});
diff --git a/packages/plugins/ui-theme/src/i18n/crowdin/ui.json b/packages/plugins/ui-theme/src/i18n/crowdin/ui.json
index da6f63f06..ff352b159 100644
--- a/packages/plugins/ui-theme/src/i18n/crowdin/ui.json
+++ b/packages/plugins/ui-theme/src/i18n/crowdin/ui.json
@@ -6,7 +6,8 @@
"visit-home-page": "Visit homepage",
"open-an-issue": "Open an issue",
"download-tarball": "Download tarball",
- "raw": "Raw Manifest"
+ "raw": "View manifest",
+ "raw-title": "Manifest of {{package}}"
},
"dialog": {
"registry-info": {
@@ -59,7 +60,7 @@
"hide-deprecated": "All deprecated versions are hidden by global configuration"
},
"package": {
- "published-on": "Published on {{time}} •",
+ "published-on": "Published {{time}}",
"version": "v{{version}}",
"visit-home-page": "Visit homepage",
"homepage": "Homepage",
@@ -84,7 +85,7 @@
},
"form-validation": {
"required-field": "This field is required",
- "required-min-length": "This field required the min length of {{length}}",
+ "required-min-length": "This field required with a minimum length of {{length}}",
"unable-to-sign-in": "Unable to sign in",
"username-or-password-cant-be-empty": "Username or password can't be empty!"
},
@@ -105,6 +106,7 @@
},
"installation": {
"title": "Installation",
+ "latest": "latest version",
"global": "global package",
"yarnModern": "yarn modern syntax"
},
@@ -115,10 +117,13 @@
"title": "Author"
},
"distribution": {
- "title": "Latest Distribution",
+ "title": "Distribution",
"license": "License",
"size": "Size",
- "file-count": "file count"
+ "file-count": "File Count"
+ },
+ "keywords": {
+ "title": "Keywords"
},
"maintainers": {
"title": "Maintainers"
diff --git a/packages/ui-components/.eslintrc b/packages/ui-components/.eslintrc
index bbc91ce2d..ff56f7914 100644
--- a/packages/ui-components/.eslintrc
+++ b/packages/ui-components/.eslintrc
@@ -60,7 +60,7 @@
"react/jsx-curly-brace-presence": ["warn", { "props": "ignore", "children": "ignore" }],
"react/jsx-pascal-case": ["error"],
"react/jsx-props-no-multi-spaces": ["error"],
- "react/jsx-sort-default-props": ["error"],
+ "react/sort-default-props": ["error"],
"react/jsx-sort-props": ["error"],
"react/no-string-refs": ["error"],
"react/no-danger-with-children": ["error"],
diff --git a/packages/ui-components/jest/api/storybook-readme.js b/packages/ui-components/jest/api/storybook-readme.js
new file mode 100644
index 000000000..6a4d56da0
--- /dev/null
+++ b/packages/ui-components/jest/api/storybook-readme.js
@@ -0,0 +1,3 @@
+module.exports = () =>
+ // eslint-disable-next-line max-len
+ '
Storybook CLI \nThis is a wrapper for https://www.npmjs.com/package/@storybook/cli
';
diff --git a/packages/ui-components/jest/api/storybook-readme.txt b/packages/ui-components/jest/api/storybook-readme.txt
deleted file mode 100644
index b4abacb53..000000000
--- a/packages/ui-components/jest/api/storybook-readme.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Storybook CLI
-This is a wrapper for https://www.npmjs.com/package/@storybook/cli
diff --git a/packages/ui-components/jest/server-handlers.ts b/packages/ui-components/jest/server-handlers.ts
index 806cbd8f7..d99142f1a 100644
--- a/packages/ui-components/jest/server-handlers.ts
+++ b/packages/ui-components/jest/server-handlers.ts
@@ -1,3 +1,4 @@
+import fs from 'fs';
import { rest } from 'msw';
const packagesPayload = require('./api/home-packages.json');
@@ -21,24 +22,20 @@ export const handlers = [
}),
rest.get('http://localhost:9000/-/verdaccio/data/package/readme/storybook', (req, res, ctx) => {
- return res(
- ctx.text(`Storybook CLI MSW.js
- This is a wrapper for https://www.npmjs.com/package/@storybook/cli
- `)
- );
+ return res(ctx.text(require('./api/storybook-readme')()));
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/jquery', (req, res, ctx) => {
return res(ctx.json(require('./api/jquery-sidebar.json')));
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/JSONStream', (req, res, ctx) => {
- return res(ctx.status(401));
+ return res(ctx.status(401)); // unauthorized
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/semver', (req, res, ctx) => {
- return res(ctx.status(500));
+ return res(ctx.status(500)); // internal server error
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/kleur', (req, res, ctx) => {
- return res(ctx.status(404));
+ return res(ctx.status(404)); // not found
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/glob', (req, res, ctx) => {
return res(ctx.json(require('./api/glob-sidebar.json')));
@@ -55,6 +52,14 @@ export const handlers = [
rest.get('http://localhost:9000/-/verdaccio/data/package/readme/jquery', (req, res, ctx) => {
return res(ctx.text(require('./api/jquery-readme')()));
}),
+ rest.get('http://localhost:9000/verdaccio/-/verdaccio-1.0.0.tgz', (req, res, ctx) => {
+ const fileContent = fs.readFileSync('./api/verdaccio-1.0.0.tgz');
+ return res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/octet-stream'),
+ ctx.body(fileContent)
+ );
+ }),
rest.post<{ username: string; password: string }, { token: string; username: string }>(
'http://localhost:9000/-/verdaccio/sec/login',
diff --git a/packages/ui-components/src/AppTest/AppRoute.test.tsx b/packages/ui-components/src/AppTest/AppRoute.test.tsx
new file mode 100644
index 000000000..e497a9c1c
--- /dev/null
+++ b/packages/ui-components/src/AppTest/AppRoute.test.tsx
@@ -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(
+
+
+ ,
+ 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'));
+ });
+});
diff --git a/packages/ui-components/src/Theme/ThemeProvider.test.tsx b/packages/ui-components/src/Theme/ThemeProvider.test.tsx
new file mode 100644
index 000000000..ed6f121e8
--- /dev/null
+++ b/packages/ui-components/src/Theme/ThemeProvider.test.tsx
@@ -0,0 +1,27 @@
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+
+import {
+ AppConfigurationProvider,
+ PersistenceSettingProvider,
+ StyleBaseline,
+ ThemeProvider,
+} from '../';
+
+const AppContainer = () => (
+
+
+
+
+ {'Theme'}
+
+
+
+);
+
+describe('ThemeProvider', () => {
+ test('should render with theme', async () => {
+ render( );
+ await screen.findByText('Theme');
+ });
+});
diff --git a/packages/ui-components/src/Theme/theme.ts b/packages/ui-components/src/Theme/theme.ts
index eb534c803..9153fc3dd 100644
--- a/packages/ui-components/src/Theme/theme.ts
+++ b/packages/ui-components/src/Theme/theme.ts
@@ -6,11 +6,11 @@ const colors = {
black: '#000',
white: '#fff',
red: '#d32f2f',
- orange: '#CD4000',
+ orange: '#cd4000',
greySuperLight: '#f5f5f5',
greyLight: '#d3d3d3',
greyLight2: '#908ba1',
- greyLight3: '#f3f4f240',
+ greyLight3: '#f3f4f2',
greyDark: '#a9a9a9',
greyDark2: '#586069',
greyChateau: '#95989a',
@@ -38,7 +38,7 @@ const themeModes = {
...colors,
primary: '#ffffff',
secondary: '#424242',
- background: '#1A202C',
+ background: '#1a202c',
},
};
diff --git a/packages/ui-components/src/components/ActionBar/ActionBar.test.tsx b/packages/ui-components/src/components/ActionBar/ActionBar.test.tsx
index 8103e0bba..001ba3b4a 100644
--- a/packages/ui-components/src/components/ActionBar/ActionBar.test.tsx
+++ b/packages/ui-components/src/components/ActionBar/ActionBar.test.tsx
@@ -1,28 +1,22 @@
import React from 'react';
import { store } from '../../store/store';
-import {
- cleanup,
- fireEvent,
- renderWithStore,
- screen,
- waitFor,
-} from '../../test/test-react-testing-library';
+import { cleanup, fireEvent, renderWithStore, screen } from '../../test/test-react-testing-library';
import ActionBar from './ActionBar';
const defaultPackageMeta = {
_uplinks: {},
latest: {
- name: 'verdaccio-ui/local-storage',
- version: '8.0.1-next.1',
+ name: 'verdaccio',
+ version: '1.0.0',
dist: {
- fileCount: 0,
- unpackedSize: 0,
- tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
+ fileCount: 1,
+ unpackedSize: 171,
+ tarball: 'http://localhost:9000/verdaccio/-/verdaccio-1.0.0.tgz',
},
homepage: 'https://verdaccio.org',
bugs: {
- url: 'https://github.com/verdaccio/monorepo/issues',
+ url: 'https://github.com/verdaccio/verdaccio/issues',
},
},
};
@@ -39,6 +33,12 @@ describe(' component', () => {
expect(screen.getByTestId('HomeIcon')).toBeInTheDocument();
});
+ test('should not render if data is missing', () => {
+ // @ts-ignore - testing with missing data
+ renderWithStore( , store);
+ expect(screen.queryByTestId('HomeIcon')).toBeNull();
+ });
+
test('when there is no action bar data', () => {
const packageMeta = {
...defaultPackageMeta,
@@ -64,6 +64,14 @@ describe(' component', () => {
expect(screen.getByLabelText('action-bar-action.download-tarball')).toBeTruthy();
});
+ test('when button to download is disabled', () => {
+ renderWithStore(
+ ,
+ store
+ );
+ expect(screen.queryByTestId('download-tarball-btn')).not.toBeInTheDocument();
+ });
+
test('when there is a button to raw manifest', () => {
renderWithStore( , store);
expect(screen.getByLabelText('action-bar-action.raw')).toBeTruthy();
@@ -71,15 +79,28 @@ describe(' component', () => {
test('when click button to raw manifest open a dialog with viewer', async () => {
renderWithStore( , store);
+ expect(screen.queryByTestId('rawViewer--dialog')).toBeFalsy();
+
fireEvent.click(screen.getByLabelText('action-bar-action.raw'));
- await waitFor(() => expect(screen.getByTestId('rawViewer--dialog')).toBeInTheDocument());
+ await screen.findByTestId('rawViewer--dialog');
+
+ fireEvent.click(screen.getByTestId('close-raw-viewer'));
+ await screen.getByLabelText('action-bar-action.raw');
+
+ expect(screen.queryByTestId('rawViewer--dialog')).toBeFalsy();
});
test('should not display download tarball button', () => {
- renderWithStore( , store);
+ renderWithStore( , store);
expect(screen.queryByLabelText('Download tarball')).toBeFalsy();
});
+ test('when click button to download ', async () => {
+ renderWithStore( , store);
+ fireEvent.click(screen.getByTestId('download-tarball-btn'));
+ await store.getState().loading.models.download;
+ });
+
test('should not display show raw button', () => {
renderWithStore( , store);
expect(screen.queryByLabelText('action-bar-action.raw')).toBeFalsy();
diff --git a/packages/ui-components/src/components/ActionBar/ActionBar.tsx b/packages/ui-components/src/components/ActionBar/ActionBar.tsx
index 6669f18a9..b7559d62d 100644
--- a/packages/ui-components/src/components/ActionBar/ActionBar.tsx
+++ b/packages/ui-components/src/components/ActionBar/ActionBar.tsx
@@ -41,7 +41,7 @@ const ActionBar: React.FC = ({ showRaw, showDownloadTarball = true, packa
}
return (
-
+
{actions.map((action) => (
diff --git a/packages/ui-components/src/components/ActionBar/ActionBarAction.tsx b/packages/ui-components/src/components/ActionBar/ActionBarAction.tsx
index f22eec6a2..4ff7fdbd3 100644
--- a/packages/ui-components/src/components/ActionBar/ActionBarAction.tsx
+++ b/packages/ui-components/src/components/ActionBar/ActionBarAction.tsx
@@ -12,11 +12,14 @@ import { useDispatch, useSelector } from 'react-redux';
import { Theme } from '../../Theme';
import { Dispatch, RootState } from '../../store/store';
-import { Link } from '../Link';
+import LinkExternal from '../LinkExternal';
export const Fab = styled(FabMUI)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor:
theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue,
+ '&:hover': {
+ color: theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue,
+ },
color: theme?.palette.white,
}));
@@ -42,21 +45,21 @@ const ActionBarAction: React.FC = ({ type, link, action })
case 'VISIT_HOMEPAGE':
return (
-
+
-
+
);
case 'OPEN_AN_ISSUE':
return (
-
+
-
+
);
case 'DOWNLOAD_TARBALL':
diff --git a/packages/ui-components/src/components/Author/Author.test.tsx b/packages/ui-components/src/components/Author/Author.test.tsx
index d62a57763..3594ca372 100644
--- a/packages/ui-components/src/components/Author/Author.test.tsx
+++ b/packages/ui-components/src/components/Author/Author.test.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { cleanup, render } from '../../test/test-react-testing-library';
+import { cleanup, render, screen } from '../../test/test-react-testing-library';
import { PackageMetaInterface } from '../../types/packageMeta';
import Authors from './Author';
@@ -66,4 +66,10 @@ describe(' component', () => {
const wrapper = render(withAuthorComponent(packageMeta));
expect(wrapper).toMatchSnapshot();
});
+
+ test('should not render if data is missing', () => {
+ // @ts-ignore - testing with missing data
+ render(withAuthorComponent(undefined));
+ expect(screen.queryByText('sidebar.author.title')).toBeNull();
+ });
});
diff --git a/packages/ui-components/src/components/Author/Author.tsx b/packages/ui-components/src/components/Author/Author.tsx
index ab0fbeab8..eb8270b41 100644
--- a/packages/ui-components/src/components/Author/Author.tsx
+++ b/packages/ui-components/src/components/Author/Author.tsx
@@ -1,35 +1,13 @@
-import { Typography } from '@mui/material';
-import Avatar from '@mui/material/Avatar';
import List from '@mui/material/List';
-import { useTheme } from '@mui/styles';
-import i18next from 'i18next';
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
-import { url } from '../../utils';
+import Person from '../Person';
import { AuthorListItem, StyledText } from './styles';
-export function getAuthorName(authorName?: string): string {
- if (!authorName) {
- return i18next.t('author-unknown');
- }
-
- if (authorName.toLowerCase() === 'anonymous') {
- return i18next.t('author-anonymous');
- }
-
- return authorName;
-}
-
-/**
- * @param param0
- * @returns
- */
const Author: FC<{ packageMeta }> = ({ packageMeta }) => {
const { t } = useTranslation();
- const theme = useTheme();
-
if (!packageMeta) {
return null;
}
@@ -40,26 +18,10 @@ const Author: FC<{ packageMeta }> = ({ packageMeta }) => {
return null;
}
- const { email, name } = author;
- const avatarComponent = (
-
- );
-
return (
{t('sidebar.author.title')}}>
-
- {!email || !url.isEmail(email) ? (
- avatarComponent
- ) : (
-
- {avatarComponent}
-
- )}
- {name && {getAuthorName(name)} }
+
+
);
diff --git a/packages/ui-components/src/components/Author/__snapshots__/Author.test.tsx.snap b/packages/ui-components/src/components/Author/__snapshots__/Author.test.tsx.snap
index 55cdbe5e4..85e76061b 100644
--- a/packages/ui-components/src/components/Author/__snapshots__/Author.test.tsx.snap
+++ b/packages/ui-components/src/components/Author/__snapshots__/Author.test.tsx.snap
@@ -44,6 +44,8 @@ exports[` component should render the component in default state 1`] =
padding-bottom: 8px;
padding-left: 16px;
padding-right: 16px;
+ margin-top: 8px;
+ margin-bottom: 8px;
padding: 0;
}
@@ -68,6 +70,22 @@ exports[` component should render the component in default state 1`] =
}
.emotion-5 {
+ margin: 0;
+ font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
+ font-weight: 400;
+ font-size: 0.75rem;
+ line-height: 1.66;
+ color: #4b5e40;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-5:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-6 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
@@ -97,10 +115,11 @@ exports[` component should render the component in default state 1`] =
user-select: none;
width: 40px;
height: 40px;
+ margin-left: 0px;
margin-right: 8px;
}
-.emotion-6 {
+.emotion-7 {
width: 100%;
height: 100%;
text-align: center;
@@ -109,12 +128,13 @@ exports[` component should render the component in default state 1`] =
text-indent: 10000px;
}
-.emotion-7 {
+.emotion-8 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.57;
+ margin-left: 8px;
}
@@ -131,21 +151,25 @@ exports[` component should render the component in default state 1`] =
class="MuiListItem-root MuiListItem-gutters MuiListItem-padding emotion-3 emotion-4"
>
verdaccio user
@@ -194,6 +218,8 @@ exports[` component should render the component in default state 1`] =
padding-bottom: 8px;
padding-left: 16px;
padding-right: 16px;
+ margin-top: 8px;
+ margin-bottom: 8px;
padding: 0;
}
@@ -218,6 +244,22 @@ exports[` component should render the component in default state 1`] =
}
.emotion-5 {
+ margin: 0;
+ font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
+ font-weight: 400;
+ font-size: 0.75rem;
+ line-height: 1.66;
+ color: #4b5e40;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-5:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-6 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
@@ -247,10 +289,11 @@ exports[` component should render the component in default state 1`] =
user-select: none;
width: 40px;
height: 40px;
+ margin-left: 0px;
margin-right: 8px;
}
-.emotion-6 {
+.emotion-7 {
width: 100%;
height: 100%;
text-align: center;
@@ -259,12 +302,13 @@ exports[` component should render the component in default state 1`] =
text-indent: 10000px;
}
-.emotion-7 {
+.emotion-8 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.57;
+ margin-left: 8px;
}
@@ -280,21 +324,25 @@ exports[`
component should render the component in default state 1`] =
class="MuiListItem-root MuiListItem-gutters MuiListItem-padding emotion-3 emotion-4"
>
verdaccio user
@@ -399,6 +447,8 @@ exports[`
component should render the component when there is no autho
padding-bottom: 8px;
padding-left: 16px;
padding-right: 16px;
+ margin-top: 8px;
+ margin-bottom: 8px;
padding: 0;
}
@@ -452,6 +502,7 @@ exports[`
component should render the component when there is no autho
user-select: none;
width: 40px;
height: 40px;
+ margin-left: 0px;
margin-right: 8px;
}
@@ -470,6 +521,7 @@ exports[`
component should render the component when there is no autho
font-weight: 500;
font-size: 0.875rem;
line-height: 1.57;
+ margin-left: 8px;
}
@@ -487,6 +539,8 @@ exports[`
component should render the component when there is no autho
>
component should render the component when there is no autho
padding-bottom: 8px;
padding-left: 16px;
padding-right: 16px;
+ margin-top: 8px;
+ margin-bottom: 8px;
padding: 0;
}
@@ -597,6 +653,7 @@ exports[`
component should render the component when there is no autho
user-select: none;
width: 40px;
height: 40px;
+ margin-left: 0px;
margin-right: 8px;
}
@@ -615,6 +672,7 @@ exports[`
component should render the component when there is no autho
font-weight: 500;
font-size: 0.875rem;
line-height: 1.57;
+ margin-left: 8px;
}
@@ -631,6 +689,8 @@ exports[`
component should render the component when there is no autho
>
Promise.resolve()) },
+});
+
+describe('CopyToClipBoard component', () => {
+ test('should copy text to clipboard', async () => {
+ const copyThis = 'copy this';
+ render(
+
+ );
+ expect(screen.getByTestId('copy-component')).toBeInTheDocument();
+
+ const copyComponent = await screen.findByTestId('copy-component');
+ await fireEvent.click(copyComponent);
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyThis);
+ });
+});
diff --git a/packages/ui-components/src/components/CopyClipboard/utils.ts b/packages/ui-components/src/components/CopyClipboard/utils.ts
index 60deb5d74..717348713 100644
--- a/packages/ui-components/src/components/CopyClipboard/utils.ts
+++ b/packages/ui-components/src/components/CopyClipboard/utils.ts
@@ -5,17 +5,6 @@ export const copyToClipBoardUtility =
(event: SyntheticEvent
): void => {
event.preventDefault();
- const node = document.createElement('div');
- node.innerText = str;
- if (document.body) {
- document.body.appendChild(node);
-
- const range = document.createRange();
- const selection = window.getSelection() as Selection;
- range.selectNodeContents(node);
- selection.removeAllRanges();
- selection.addRange(range);
- document.execCommand('copy');
- document.body.removeChild(node);
- }
+ // document.execCommand is deprecated
+ navigator.clipboard.writeText(str);
};
diff --git a/packages/ui-components/src/components/Dependencies/Dependencies.tsx b/packages/ui-components/src/components/Dependencies/Dependencies.tsx
index 49382ab36..232e8b9c5 100644
--- a/packages/ui-components/src/components/Dependencies/Dependencies.tsx
+++ b/packages/ui-components/src/components/Dependencies/Dependencies.tsx
@@ -1,19 +1,13 @@
-import styled from '@emotion/styled';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import React, { Fragment } from 'react';
import { useTranslation } from 'react-i18next';
-import { Theme } from '../../Theme';
import NoItems from '../NoItems';
import { DependencyBlock } from './DependencyBlock';
import { hasKeys } from './utits';
-export const CardWrap = styled(Card)<{ theme?: Theme }>((props) => ({
- marginBottom: props.theme.spacing(2),
-}));
-
const Dependencies: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
const { t } = useTranslation();
@@ -43,7 +37,7 @@ const Dependencies: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
hasKeys(peerDependencies);
if (hasDependencies) {
return (
-
+
{Object.entries(dependencyMap).map(([dependencyType, dependencies]) => {
@@ -62,11 +56,17 @@ const Dependencies: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
})}
-
+
);
}
- return ;
+ return (
+
+
+
+
+
+ );
};
export default Dependencies;
diff --git a/packages/ui-components/src/components/Dependencies/DependencyBlock.tsx b/packages/ui-components/src/components/Dependencies/DependencyBlock.tsx
index 9ca871ecf..ebe2c9071 100644
--- a/packages/ui-components/src/components/Dependencies/DependencyBlock.tsx
+++ b/packages/ui-components/src/components/Dependencies/DependencyBlock.tsx
@@ -9,6 +9,7 @@ import { useHistory } from 'react-router-dom';
import { Theme } from '../../Theme';
import { PackageDependencies } from '../../types/packageMeta';
+import { Route } from '../../utils';
interface DependencyBlockProps {
title: string;
@@ -41,7 +42,7 @@ export const DependencyBlock: React.FC = ({ title, depende
const deps = Object.entries(dependencies);
function handleClick(name: string): void {
- history.push(`/-/web/detail/${name}`);
+ history.push(`${Route.DETAIL}${name}`);
}
function labelText(title: string, name: string, version: string): string {
diff --git a/packages/ui-components/src/components/Developers/DeveloperType.tsx b/packages/ui-components/src/components/Developers/DeveloperType.tsx
new file mode 100644
index 000000000..1ae0f6165
--- /dev/null
+++ b/packages/ui-components/src/components/Developers/DeveloperType.tsx
@@ -0,0 +1,4 @@
+export enum DeveloperType {
+ CONTRIBUTORS = 'contributors',
+ MAINTAINERS = 'maintainers',
+}
diff --git a/packages/ui-components/src/components/Developers/Developers.test.tsx b/packages/ui-components/src/components/Developers/Developers.test.tsx
index dfc341a91..f995522e8 100644
--- a/packages/ui-components/src/components/Developers/Developers.test.tsx
+++ b/packages/ui-components/src/components/Developers/Developers.test.tsx
@@ -1,8 +1,8 @@
import React from 'react';
-import { cleanup, fireEvent, render } from '../../test/test-react-testing-library';
+import { cleanup, fireEvent, render, screen } from '../../test/test-react-testing-library';
+import { DeveloperType } from './DeveloperType';
import Developers from './Developers';
-import { DeveloperType } from './Title';
describe('test Developers', () => {
afterEach(() => {
@@ -63,7 +63,7 @@ describe('test Developers', () => {
expect(wrapper).toMatchSnapshot();
});
- test('should test onClick the component avatar', () => {
+ test('should show only up to max items', () => {
const packageMeta = {
latest: {
packageName: 'foo',
@@ -72,6 +72,7 @@ describe('test Developers', () => {
{
name: 'dmethvin',
email: 'test@gmail.com',
+ url: 'https://example.com/',
},
{
name: 'dmethvin2',
@@ -89,14 +90,40 @@ describe('test Developers', () => {
);
- // const item2 = wrapper.find(Fab);
- // // TODO: I am not sure here how to verify the method inside the component was called.
- // item2.simulate('click');
-
expect(wrapper.getByText('sidebar.contributors.title')).toBeInTheDocument();
- fireEvent.click(wrapper.getByRole('button'));
+ expect(wrapper.getByTestId(packageMeta.latest.contributors[0].name)).toBeInTheDocument();
+ expect(wrapper.queryByTestId(packageMeta.latest.contributors[1].name)).not.toBeInTheDocument();
+ expect(wrapper.queryByTestId(packageMeta.latest.contributors[2].name)).not.toBeInTheDocument();
+ });
- expect(wrapper.getByLabelText(packageMeta.latest.contributors[0].name)).toBeInTheDocument();
- expect(wrapper.getByLabelText(packageMeta.latest.contributors[1].name)).toBeInTheDocument();
+ test('renders only the first six contributors when there are more than six', () => {
+ const packageMeta = {
+ latest: {
+ contributors: [
+ { name: 'contributor1', email: 'c1@test.com' },
+ { name: 'contributor2', email: 'c2@test.com' },
+ { name: 'contributor3', email: 'c3@test.com' },
+ { name: 'contributor4', email: 'c4@test.com' },
+ { name: 'contributor5', email: 'c5@test.com' },
+ { name: 'contributor6', email: 'c6@test.com' },
+ { name: 'contributor7', email: 'c7@test.com' },
+ ],
+ },
+ };
+
+ render( );
+
+ 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();
});
});
diff --git a/packages/ui-components/src/components/Developers/Developers.tsx b/packages/ui-components/src/components/Developers/Developers.tsx
index fdeae867e..c0f027854 100644
--- a/packages/ui-components/src/components/Developers/Developers.tsx
+++ b/packages/ui-components/src/components/Developers/Developers.tsx
@@ -1,20 +1,15 @@
import styled from '@emotion/styled';
import Add from '@mui/icons-material/Add';
-import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import FabMUI from '@mui/material/Fab';
-import Tooltip from '@mui/material/Tooltip';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Theme } from '../../Theme';
+import Person from '../Person';
+import { DeveloperType } from './DeveloperType';
import Title from './Title';
import getUniqueDeveloperValues from './get-unique-developer-values';
-export enum DeveloperType {
- CONTRIBUTORS = 'contributors',
- MAINTAINERS = 'maintainers',
-}
-
export const Fab = styled(FabMUI)<{ theme?: Theme }>((props) => ({
backgroundColor: props.theme?.palette.primary.main,
color: props.theme?.palette.white,
@@ -28,7 +23,7 @@ interface Props {
const StyledBox = styled(Box)({
'> *': {
- margin: 5,
+ marginRight: 5,
},
});
@@ -58,19 +53,24 @@ const Developers: React.FC = ({ type, visibleMax = VISIBLE_MAX, packageMe
return null;
}
+ const { name: packageName, version } = packageMeta.latest;
+
return (
<>
- {visibleDevelopers.map((visibleDeveloper) => {
+ {visibleDevelopers.map((visibleDeveloper, index) => {
return (
-
-
-
+
);
})}
{visibleDevelopersMax < developers.length && (
-
+
)}
diff --git a/packages/ui-components/src/components/Developers/Title.tsx b/packages/ui-components/src/components/Developers/Title.tsx
index 2e304ba0b..f4579457c 100644
--- a/packages/ui-components/src/components/Developers/Title.tsx
+++ b/packages/ui-components/src/components/Developers/Title.tsx
@@ -4,11 +4,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Theme } from '../../Theme';
-
-export enum DeveloperType {
- CONTRIBUTORS = 'contributors',
- MAINTAINERS = 'maintainers',
-}
+import { DeveloperType } from './';
interface Props {
type: DeveloperType;
diff --git a/packages/ui-components/src/components/Developers/__snapshots__/Developers.test.tsx.snap b/packages/ui-components/src/components/Developers/__snapshots__/Developers.test.tsx.snap
index c7c50e1a6..1645dc7ae 100644
--- a/packages/ui-components/src/components/Developers/__snapshots__/Developers.test.tsx.snap
+++ b/packages/ui-components/src/components/Developers/__snapshots__/Developers.test.tsx.snap
@@ -26,10 +26,26 @@ exports[`test Developers should render the component for contributors with items
}
.emotion-3>* {
- margin: 5px;
+ margin-right: 5px;
}
.emotion-4 {
+ margin: 0;
+ font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
+ font-weight: 400;
+ font-size: 0.75rem;
+ line-height: 1.66;
+ color: #4b5e40;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-4:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-5 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
@@ -59,9 +75,13 @@ exports[`test Developers should render the component for contributors with items
user-select: none;
color: #f4f4f4;
background-color: #bdbdbd;
+ width: 40px;
+ height: 40px;
+ margin-left: 0px;
+ margin-right: 8px;
}
-.emotion-5 {
+.emotion-6 {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -90,40 +110,54 @@ exports[`test Developers should render the component for contributors with items
+
+
+
+
+
,
@@ -150,10 +184,26 @@ exports[`test Developers should render the component for contributors with items
}
.emotion-3>* {
- margin: 5px;
+ margin-right: 5px;
}
.emotion-4 {
+ margin: 0;
+ font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
+ font-weight: 400;
+ font-size: 0.75rem;
+ line-height: 1.66;
+ color: #4b5e40;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-4:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-5 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
@@ -183,9 +233,13 @@ exports[`test Developers should render the component for contributors with items
user-select: none;
color: #f4f4f4;
background-color: #bdbdbd;
+ width: 40px;
+ height: 40px;
+ margin-left: 0px;
+ margin-right: 8px;
}
-.emotion-5 {
+.emotion-6 {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -213,40 +267,54 @@ exports[`test Developers should render the component for contributors with items
+
+
+
+
+
,
"debug": [Function],
@@ -329,10 +397,26 @@ exports[`test Developers should render the component for maintainers with items
}
.emotion-3>* {
- margin: 5px;
+ margin-right: 5px;
}
.emotion-4 {
+ margin: 0;
+ font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
+ font-weight: 400;
+ font-size: 0.75rem;
+ line-height: 1.66;
+ color: #4b5e40;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-4:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-5 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
@@ -362,9 +446,13 @@ exports[`test Developers should render the component for maintainers with items
user-select: none;
color: #f4f4f4;
background-color: #bdbdbd;
+ width: 40px;
+ height: 40px;
+ margin-left: 0px;
+ margin-right: 8px;
}
-.emotion-5 {
+.emotion-6 {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -393,40 +481,54 @@ exports[`test Developers should render the component for maintainers with items
+
+
+
+
+
,
@@ -453,10 +555,26 @@ exports[`test Developers should render the component for maintainers with items
}
.emotion-3>* {
- margin: 5px;
+ margin-right: 5px;
}
.emotion-4 {
+ margin: 0;
+ font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
+ font-weight: 400;
+ font-size: 0.75rem;
+ line-height: 1.66;
+ color: #4b5e40;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-4:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-5 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
@@ -486,9 +604,13 @@ exports[`test Developers should render the component for maintainers with items
user-select: none;
color: #f4f4f4;
background-color: #bdbdbd;
+ width: 40px;
+ height: 40px;
+ margin-left: 0px;
+ margin-right: 8px;
}
-.emotion-5 {
+.emotion-6 {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -516,40 +638,54 @@ exports[`test Developers should render the component for maintainers with items
+
+
+
+
+
,
"debug": [Function],
diff --git a/packages/ui-components/src/components/Developers/index.ts b/packages/ui-components/src/components/Developers/index.ts
index 27846f2f9..0c70a0c17 100644
--- a/packages/ui-components/src/components/Developers/index.ts
+++ b/packages/ui-components/src/components/Developers/index.ts
@@ -1 +1,2 @@
-export { default, DeveloperType } from './Developers';
+export { default } from './Developers';
+export { DeveloperType } from './DeveloperType';
diff --git a/packages/ui-components/src/components/Developers/styles.ts b/packages/ui-components/src/components/Developers/styles.ts
deleted file mode 100644
index 1dc2b07ec..000000000
--- a/packages/ui-components/src/components/Developers/styles.ts
+++ /dev/null
@@ -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,
-}));
diff --git a/packages/ui-components/src/components/Distribution/Dist.test.tsx b/packages/ui-components/src/components/Distribution/Dist.test.tsx
index 7c1a312ff..db7d68097 100644
--- a/packages/ui-components/src/components/Distribution/Dist.test.tsx
+++ b/packages/ui-components/src/components/Distribution/Dist.test.tsx
@@ -33,7 +33,7 @@ describe(' component', () => {
expect(getByText('sidebar.distribution.size')).toBeInTheDocument();
expect(getByText('sidebar.distribution.size')).toBeInTheDocument();
expect(getByText('7', { exact: false })).toBeInTheDocument();
- expect(getByText('10.00 Bytes', { exact: false })).toBeInTheDocument();
+ expect(getByText('10 Bytes', { exact: false })).toBeInTheDocument();
});
test('should render the component with license as string', () => {
diff --git a/packages/ui-components/src/components/Distribution/Dist.tsx b/packages/ui-components/src/components/Distribution/Dist.tsx
index 87e153bf4..d8163483c 100644
--- a/packages/ui-components/src/components/Distribution/Dist.tsx
+++ b/packages/ui-components/src/components/Distribution/Dist.tsx
@@ -3,8 +3,8 @@ import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { PackageMetaInterface } from '../../types/packageMeta';
+import { fileSizeSI, formatLicense } from '../../utils/utils';
import { DistChips, DistListItem, StyledText } from './styles';
-import { fileSizeSI, formatLicense } from './utils';
const DistChip: FC<{ name: string; children?: React.ReactElement | string }> = ({
name,
@@ -47,9 +47,7 @@ const Dist: FC<{ packageMeta: PackageMetaInterface }> = ({ packageMeta }) => {
{fileSizeSI(dist.unpackedSize)}
) : null}
-
- {formatLicense(license as string)}
-
+ {formatLicense(license)}
);
diff --git a/packages/ui-components/src/components/Distribution/styles.ts b/packages/ui-components/src/components/Distribution/styles.ts
index 0e15f4680..55f8526a8 100644
--- a/packages/ui-components/src/components/Distribution/styles.ts
+++ b/packages/ui-components/src/components/Distribution/styles.ts
@@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import Chip from '@mui/material/Chip';
-import FabMUI from '@mui/material/Fab';
import ListItem from '@mui/material/ListItem';
import Typography from '@mui/material/Typography';
@@ -22,8 +21,3 @@ export const DistChips = styled(Chip)({
textTransform: 'capitalize',
marginTop: 5,
});
-
-export const DownloadButton = styled(FabMUI)<{ theme?: Theme }>((props) => ({
- backgroundColor: props.theme?.palette.primary.main,
- color: props.theme?.palette.white,
-}));
diff --git a/packages/ui-components/src/components/Distribution/utils.ts b/packages/ui-components/src/components/Distribution/utils.ts
deleted file mode 100644
index 249c7e4c7..000000000
--- a/packages/ui-components/src/components/Distribution/utils.ts
+++ /dev/null
@@ -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')
- );
-}
diff --git a/packages/ui-components/src/components/Distribution/utilts.spec.ts b/packages/ui-components/src/components/Distribution/utilts.spec.ts
deleted file mode 100644
index cbec4449c..000000000
--- a/packages/ui-components/src/components/Distribution/utilts.spec.ts
+++ /dev/null
@@ -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');
-});
diff --git a/packages/ui-components/src/components/Engines/Engines.tsx b/packages/ui-components/src/components/Engines/Engines.tsx
index 34803cdeb..2cc2fda98 100644
--- a/packages/ui-components/src/components/Engines/Engines.tsx
+++ b/packages/ui-components/src/components/Engines/Engines.tsx
@@ -23,8 +23,12 @@ const EngineItem: FC = ({ title, element, engineText }) => (
{title}}>
- {element}
- {engineText}
+
+ {element}
+
+
+ {engineText}
+
diff --git a/packages/ui-components/src/components/Engines/styles.ts b/packages/ui-components/src/components/Engines/styles.ts
index c8ed2ce6b..390edbfcb 100644
--- a/packages/ui-components/src/components/Engines/styles.ts
+++ b/packages/ui-components/src/components/Engines/styles.ts
@@ -10,5 +10,5 @@ export const StyledText = styled(Typography)<{ theme?: Theme }>((props) => ({
}));
export const EngineListItem = styled(ListItem)({
- paddingLeft: 0,
+ padding: 0,
});
diff --git a/packages/ui-components/src/components/ErrorBoundary/ErrorBoundary.test.tsx b/packages/ui-components/src/components/ErrorBoundary/ErrorBoundary.test.tsx
new file mode 100644
index 000000000..d535bc629
--- /dev/null
+++ b/packages/ui-components/src/components/ErrorBoundary/ErrorBoundary.test.tsx
@@ -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(
+
+ {'Test'}
+
+ );
+
+ 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(
+
+
+
+ );
+
+ expect(screen.getByText('Something went wrong.')).toBeInTheDocument();
+ expect(screen.getByText(/error:/)).toBeInTheDocument();
+ expect(screen.getByText(/info:/)).toBeInTheDocument();
+
+ // Restore console.error after test
+ spy.mockRestore();
+ });
+});
diff --git a/packages/ui-components/src/components/FundButton/FundButton.tsx b/packages/ui-components/src/components/FundButton/FundButton.tsx
index 503dd1575..0c4323d5d 100644
--- a/packages/ui-components/src/components/FundButton/FundButton.tsx
+++ b/packages/ui-components/src/components/FundButton/FundButton.tsx
@@ -6,11 +6,11 @@ import { Trans } from 'react-i18next';
import { Theme } from '../../Theme';
import { url } from '../../utils';
-import { Link } from '../Link';
+import LinkExternal from '../LinkExternal';
-const StyledLink = styled(Link)<{ theme?: Theme }>(({ theme }) => ({
- marginTop: theme?.spacing(1),
- marginBottom: theme?.spacing(1),
+const StyledLink = styled(LinkExternal)<{ theme?: Theme }>(({ theme }) => ({
+ marginTop: theme?.spacing(2),
+ marginBottom: theme?.spacing(2),
textDecoration: 'none',
display: 'block',
}));
@@ -32,7 +32,7 @@ const FundButton: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
}
return (
-
+
{
+ const onCloseDialog = jest.fn();
+
+ const tabs = [{ label: 'Tab 1' }, { label: 'Tab 2' }];
+
+ const tabPanels = [{ element: {'Panel 1'}
}, { element: {'Panel 2'}
}];
+
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ 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();
+ });
+});
diff --git a/packages/ui-components/src/components/HeaderInfoDialog/HeaderInfoDialog.tsx b/packages/ui-components/src/components/HeaderInfoDialog/HeaderInfoDialog.tsx
index 68b006e0c..27a1498bb 100644
--- a/packages/ui-components/src/components/HeaderInfoDialog/HeaderInfoDialog.tsx
+++ b/packages/ui-components/src/components/HeaderInfoDialog/HeaderInfoDialog.tsx
@@ -54,7 +54,7 @@ const HeaderInfoDialog: React.FC = ({
-
+
{tabs
? tabs.map((item, index) => {
return (
@@ -68,7 +68,7 @@ const HeaderInfoDialog: React.FC = ({
{tabPanels
? tabPanels.map((item, index) => {
return (
-
+
{item.element}
);
diff --git a/packages/ui-components/src/components/Heading/Heading.test.tsx b/packages/ui-components/src/components/Heading/Heading.test.tsx
new file mode 100644
index 000000000..30bd0e3ce
--- /dev/null
+++ b/packages/ui-components/src/components/Heading/Heading.test.tsx
@@ -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({'Test'} );
+ const headingElement = screen.getByText('Test');
+ expect(headingElement).toBeInTheDocument();
+ expect(headingElement.tagName).toBe('H6');
+ });
+
+ test('should render correctly with custom props', () => {
+ render({'Test'} );
+ const headingElement = screen.getByText('Test');
+ expect(headingElement).toBeInTheDocument();
+ expect(headingElement.tagName).toBe('H1');
+ });
+});
diff --git a/packages/ui-components/src/components/Icons/DevsIcons/CommonJS.tsx b/packages/ui-components/src/components/Icons/DevsIcons/CommonJS.tsx
index 7bff0b797..6a838ca52 100644
--- a/packages/ui-components/src/components/Icons/DevsIcons/CommonJS.tsx
+++ b/packages/ui-components/src/components/Icons/DevsIcons/CommonJS.tsx
@@ -1,13 +1,7 @@
-import styled from '@emotion/styled';
-import { Theme } from '@mui/material';
import React from 'react';
const icon = require('./commonjs.svg');
-const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
- marginLeft: theme?.spacing(1),
-}));
-
export function CommonJS() {
- return ;
+ return ;
}
diff --git a/packages/ui-components/src/components/Icons/DevsIcons/ES6Module.tsx b/packages/ui-components/src/components/Icons/DevsIcons/ES6Module.tsx
index dc9c8418f..745cec670 100644
--- a/packages/ui-components/src/components/Icons/DevsIcons/ES6Module.tsx
+++ b/packages/ui-components/src/components/Icons/DevsIcons/ES6Module.tsx
@@ -1,13 +1,7 @@
-import styled from '@emotion/styled';
-import { Theme } from '@mui/material';
import React from 'react';
-const icon = require('./es6modules.svg');
-
-const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
- marginLeft: theme?.spacing(1),
-}));
+const icon = require('./es6module.svg');
export function ES6Modules() {
- return ;
+ return ;
}
diff --git a/packages/ui-components/src/components/Icons/DevsIcons/Git.tsx b/packages/ui-components/src/components/Icons/DevsIcons/Git.tsx
index 0cb9e1a0c..3c6e59be2 100644
--- a/packages/ui-components/src/components/Icons/DevsIcons/Git.tsx
+++ b/packages/ui-components/src/components/Icons/DevsIcons/Git.tsx
@@ -1,13 +1,7 @@
-import styled from '@emotion/styled';
-import { Theme } from '@mui/material';
import React from 'react';
const icon = require('./git.svg');
-const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
- marginLeft: theme?.spacing(1),
-}));
-
export function Git() {
- return ;
+ return ;
}
diff --git a/packages/ui-components/src/components/Icons/DevsIcons/NodeJS.tsx b/packages/ui-components/src/components/Icons/DevsIcons/NodeJS.tsx
index 079a889d8..4aad7c22f 100644
--- a/packages/ui-components/src/components/Icons/DevsIcons/NodeJS.tsx
+++ b/packages/ui-components/src/components/Icons/DevsIcons/NodeJS.tsx
@@ -1,13 +1,7 @@
-import styled from '@emotion/styled';
-import { Theme } from '@mui/material';
import React from 'react';
const icon = require('./nodejs.svg');
-const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
- marginLeft: theme?.spacing(1),
-}));
-
export function NodeJS() {
- return ;
+ return ;
}
diff --git a/packages/ui-components/src/components/Icons/DevsIcons/TypeScript.tsx b/packages/ui-components/src/components/Icons/DevsIcons/TypeScript.tsx
index f1b3483c4..2757cbbeb 100644
--- a/packages/ui-components/src/components/Icons/DevsIcons/TypeScript.tsx
+++ b/packages/ui-components/src/components/Icons/DevsIcons/TypeScript.tsx
@@ -1,13 +1,7 @@
-import styled from '@emotion/styled';
-import { Theme } from '@mui/material';
import React from 'react';
const icon = require('./typescript.svg');
-const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
- marginLeft: theme?.spacing(1),
-}));
-
export function TypeScript() {
- return ;
+ return ;
}
diff --git a/packages/ui-components/src/components/Icons/DevsIcons/es6modules.svg b/packages/ui-components/src/components/Icons/DevsIcons/es6module.svg
similarity index 100%
rename from packages/ui-components/src/components/Icons/DevsIcons/es6modules.svg
rename to packages/ui-components/src/components/Icons/DevsIcons/es6module.svg
diff --git a/packages/ui-components/src/components/Icons/FileBinary.tsx b/packages/ui-components/src/components/Icons/FileBinary.tsx
index 671860348..f1d6adf32 100644
--- a/packages/ui-components/src/components/Icons/FileBinary.tsx
+++ b/packages/ui-components/src/components/Icons/FileBinary.tsx
@@ -9,7 +9,8 @@ const FileBinary = React.forwardRef(function FileBinary(
ref: React.Ref
) {
return (
-
+ // eslint-disable-next-line verdaccio/jsx-spread
+
);
diff --git a/packages/ui-components/src/components/Icons/Icons.stories.tsx b/packages/ui-components/src/components/Icons/Icons.stories.tsx
index c5559248c..ee93cf83a 100644
--- a/packages/ui-components/src/components/Icons/Icons.stories.tsx
+++ b/packages/ui-components/src/components/Icons/Icons.stories.tsx
@@ -6,13 +6,17 @@ import {
CommonJS,
ES6Modules,
Earth,
+ FileBinary,
Git,
Law,
License,
NodeJS,
+ Npm,
+ Pnpm,
Time,
TypeScript,
Version,
+ Yarn,
} from '.';
export default {
@@ -21,16 +25,28 @@ export default {
export const Icons: any = () => (
-
+
+
+
+
+
+
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui-components/src/components/Icons/Icons.test.tsx b/packages/ui-components/src/components/Icons/Icons.test.tsx
new file mode 100644
index 000000000..706a3aab7
--- /dev/null
+++ b/packages/ui-components/src/components/Icons/Icons.test.tsx
@@ -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(
+ <>
+
+
+
+
+
+
+ >
+ );
+ expect(container.querySelectorAll('svg')).toHaveLength(6);
+ });
+
+ test('should render an IMG graphic linking to and SVG', () => {
+ const { container } = render(
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+ expect(container.querySelectorAll('img')).toHaveLength(8);
+ });
+
+ test('should render small graphic', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container.querySelector('svg')).toHaveStyle('width: 14px');
+ });
+
+ test('should render medium graphic', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container.querySelector('svg')).toHaveStyle('width: 18px');
+ });
+});
diff --git a/packages/ui-components/src/components/Icons/Law.tsx b/packages/ui-components/src/components/Icons/Law.tsx
index c63369010..dba367d9e 100644
--- a/packages/ui-components/src/components/Icons/Law.tsx
+++ b/packages/ui-components/src/components/Icons/Law.tsx
@@ -6,7 +6,8 @@ type Props = React.ComponentProps;
const Law = React.forwardRef(function Law(props: Props, ref: React.Ref) {
return (
-
+ // eslint-disable-next-line verdaccio/jsx-spread
+
(({ theme }) => ({
- marginLeft: theme?.spacing(1),
-}));
-
export function Npm() {
- return ;
+ return ;
}
diff --git a/packages/ui-components/src/components/Icons/Managers/Pnpm.tsx b/packages/ui-components/src/components/Icons/Managers/Pnpm.tsx
index 4f39d2cd9..f447b9422 100644
--- a/packages/ui-components/src/components/Icons/Managers/Pnpm.tsx
+++ b/packages/ui-components/src/components/Icons/Managers/Pnpm.tsx
@@ -1,13 +1,7 @@
-import styled from '@emotion/styled';
-import { Theme } from '@mui/material';
import React from 'react';
const icon = require('./pnpm.svg');
-const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
- marginLeft: theme?.spacing(1),
-}));
-
export function Pnpm() {
- return ;
+ return ;
}
diff --git a/packages/ui-components/src/components/Icons/Managers/Yarn.tsx b/packages/ui-components/src/components/Icons/Managers/Yarn.tsx
index e251a4c5b..0e9f99d9f 100644
--- a/packages/ui-components/src/components/Icons/Managers/Yarn.tsx
+++ b/packages/ui-components/src/components/Icons/Managers/Yarn.tsx
@@ -1,13 +1,7 @@
-import styled from '@emotion/styled';
-import { Theme } from '@mui/material';
import React from 'react';
const icon = require('./yarn.svg');
-const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({
- marginLeft: theme?.spacing(1),
-}));
-
export function Yarn() {
- return ;
+ return ;
}
diff --git a/packages/ui-components/src/components/Install/Install.test.tsx b/packages/ui-components/src/components/Install/Install.test.tsx
index 4e1747ff1..94d937a79 100644
--- a/packages/ui-components/src/components/Install/Install.test.tsx
+++ b/packages/ui-components/src/components/Install/Install.test.tsx
@@ -5,12 +5,14 @@ import { PackageManagers } from '@verdaccio/types';
import { useConfig } from '../../providers';
import { render, screen } from '../../test/test-react-testing-library';
import Install from './Install';
-import { getGlobalInstall } from './InstallListItem';
+import InstallListItem, { DependencyManager, getGlobalInstall } from './InstallListItem';
import data from './__partials__/data.json';
-const ComponentToBeRendered: React.FC<{ pkgManagers?: PackageManagers[] }> = () => {
+const ComponentToBeRendered: React.FC<{ name?: string; pkgManagers?: PackageManagers[] }> = ({
+ name = 'foo',
+}) => {
const { configOptions } = useConfig();
- return ;
+ return ;
};
/* eslint-disable react/jsx-no-bind*/
@@ -22,6 +24,11 @@ describe(' ', () => {
expect(screen.getByText('npm install foo@8.0.0')).toBeInTheDocument();
});
+ test('should not render if name is missing', () => {
+ render( );
+ expect(screen.queryByTestId('installList')).toBeNull();
+ });
+
test('should have 3 children', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['yarn', 'pnpm', 'npm'];
const { getByTestId } = render( );
@@ -32,9 +39,7 @@ describe(' ', () => {
test('should have the element NPM', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['npm'];
-
render( );
-
expect(screen.getByText('sidebar.installation.title')).toBeTruthy();
expect(screen.queryByText('pnpm')).not.toBeInTheDocument();
expect(screen.queryByText('yarn')).not.toBeInTheDocument();
@@ -59,18 +64,54 @@ describe(' ', () => {
});
});
-describe('getGlobalInstall', () => {
- test('no global', () => {
- expect(getGlobalInstall(false, 'foo', '1.0.0')).toEqual('1.0.0@foo');
- });
- test('global', () => {
- expect(getGlobalInstall(true, 'foo', '1.0.0')).toEqual('-g 1.0.0@foo');
+describe(' ', () => {
+ test('renders correctly', () => {
+ render(
+
+ );
+ expect(screen.queryByTestId('installListItem-npm')).toBeInTheDocument();
});
- test('yarn no global', () => {
- expect(getGlobalInstall(false, 'foo', '1.0.0', true)).toEqual('1.0.0@foo');
- });
- test('yarn global', () => {
- expect(getGlobalInstall(true, 'foo', '1.0.0', true)).toEqual('1.0.0@foo');
+ test('should not render if name is missing', () => {
+ render(
+ // @ts-ignore - testing invalid value
+
+ );
+ // 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');
});
});
diff --git a/packages/ui-components/src/components/Install/Install.tsx b/packages/ui-components/src/components/Install/Install.tsx
index 7e97d3f10..30f67cf9d 100644
--- a/packages/ui-components/src/components/Install/Install.tsx
+++ b/packages/ui-components/src/components/Install/Install.tsx
@@ -1,8 +1,6 @@
import styled from '@emotion/styled';
import { Typography } from '@mui/material';
-import Grid from '@mui/material/Grid';
import List from '@mui/material/List';
-import { useTheme } from '@mui/styles';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -18,6 +16,12 @@ const StyledText = styled(Typography)<{ theme?: Theme }>((props) => ({
textTransform: 'capitalize',
}));
+const Wrapper = styled('div')({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+});
+
export type Props = {
packageMeta: PackageMetaInterface;
packageName: string;
@@ -26,7 +30,6 @@ export type Props = {
const Install: React.FC = ({ packageMeta, packageName, configOptions }) => {
const { t } = useTranslation();
- const theme = useTheme();
if (!packageMeta || !packageName) {
return null;
}
@@ -38,16 +41,14 @@ const Install: React.FC = ({ packageMeta, packageName, configOptions }) =
return hasPkgManagers ? (
<>
-
-
-
{t('sidebar.installation.title')}}
+ subheader={
+
+ {t('sidebar.installation.title')}
+
+
+ }
>
{hasNpm && (
= ({
}) => {
const { localSettings } = useSettings();
const theme = useTheme();
+ const isLatest = localSettings[packageName]?.latest ?? false;
const isGlobal = localSettings[packageName]?.global ?? false;
switch (dependencyManager) {
case DependencyManager.NPM:
@@ -67,9 +68,9 @@ const InstallListItem: React.FC = ({
}
/>
@@ -93,7 +94,7 @@ const InstallListItem: React.FC = ({
packageName,
true
)}`
- : `yarn add ${getGlobalInstall(isGlobal, packageVersion, packageName, true)}`
+ : `yarn add ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, true)}`
}
title={
isGlobal
@@ -103,7 +104,7 @@ const InstallListItem: React.FC = ({
packageName,
true
)}`
- : `yarn add ${getGlobalInstall(isGlobal, packageVersion, packageName, true)}`
+ : `yarn add ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, true)}`
}
/>
}
@@ -120,8 +121,8 @@ const InstallListItem: React.FC = ({
primary={
}
/>
diff --git a/packages/ui-components/src/components/Keywords/KeywordListItems.tsx b/packages/ui-components/src/components/Keywords/KeywordListItems.tsx
new file mode 100644
index 000000000..d9f6c9597
--- /dev/null
+++ b/packages/ui-components/src/components/Keywords/KeywordListItems.tsx
@@ -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 (
+
+ {keywordList.sort().map((keyword, index) => (
+
+ ))}
+
+ );
+};
+
+export default KeywordListItems;
diff --git a/packages/ui-components/src/components/Keywords/Keywords.stories.tsx b/packages/ui-components/src/components/Keywords/Keywords.stories.tsx
new file mode 100644
index 000000000..05c0466df
--- /dev/null
+++ b/packages/ui-components/src/components/Keywords/Keywords.stories.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import { default as Keywords } from '.';
+
+export default {
+ title: 'Components/Sidebar/Keywords',
+};
+
+export const AllProperties: any = () => (
+
+);
diff --git a/packages/ui-components/src/components/Keywords/Keywords.test.tsx b/packages/ui-components/src/components/Keywords/Keywords.test.tsx
new file mode 100644
index 000000000..b70a067f0
--- /dev/null
+++ b/packages/ui-components/src/components/Keywords/Keywords.test.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+
+import { render, screen } from '../../test/test-react-testing-library';
+import Keywords from './Keywords';
+
+describe(' 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( );
+
+ 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( );
+ expect(screen.queryByTestId('keyword-list')).toBeNull();
+
+ const packageMeta = {
+ latest: {
+ name: 'verdaccio1',
+ version: '4.0.0',
+ keywords: '',
+ },
+ };
+
+ render( );
+ 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( );
+
+ expect(container.getByText('sidebar.keywords.title')).toBeInTheDocument();
+ expect(container.getByText('verdaccio')).toBeInTheDocument();
+ expect(container.getByText('hello')).toBeInTheDocument();
+ expect(container.getByText('world')).toBeInTheDocument();
+ });
+});
diff --git a/packages/ui-components/src/components/Keywords/Keywords.tsx b/packages/ui-components/src/components/Keywords/Keywords.tsx
new file mode 100644
index 000000000..86f8ed5f0
--- /dev/null
+++ b/packages/ui-components/src/components/Keywords/Keywords.tsx
@@ -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 (
+
+ {t('sidebar.keywords.title')}
+
+ }
+ >
+
+
+ );
+};
+
+export default Keywords;
diff --git a/packages/ui-components/src/components/Keywords/index.ts b/packages/ui-components/src/components/Keywords/index.ts
new file mode 100644
index 000000000..eec7027cf
--- /dev/null
+++ b/packages/ui-components/src/components/Keywords/index.ts
@@ -0,0 +1 @@
+export { default } from './Keywords';
diff --git a/packages/ui-components/src/components/Link/Link.test.tsx b/packages/ui-components/src/components/Link/Link.test.tsx
new file mode 100644
index 000000000..f555690b5
--- /dev/null
+++ b/packages/ui-components/src/components/Link/Link.test.tsx
@@ -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(' component', () => {
+ test('should render the component in default state', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ test('should render the component with link', () => {
+ const { container } = render(
+
+ {'Home'}
+
+ );
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/packages/ui-components/src/components/Link/Link.tsx b/packages/ui-components/src/components/Link/Link.tsx
index eac322e5e..5e5a66d1b 100644
--- a/packages/ui-components/src/components/Link/Link.tsx
+++ b/packages/ui-components/src/components/Link/Link.tsx
@@ -3,9 +3,7 @@ import Typography from '@mui/material/Typography';
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
-type LinkRef = HTMLAnchorElement;
-
-export const CustomRouterLink = styled(RouterLink)`
+const CustomRouterLink = styled(RouterLink)`
text-decoration: none;
&:hover,
&:focus {
@@ -13,23 +11,11 @@ export const CustomRouterLink = styled(RouterLink)`
}
`;
-// TODO: improve any with custom types for a and RouterLink
-const Link = React.forwardRef(function LinkFunction(
- { external, to, children, variant, className, onClick },
+const Link = React.forwardRef(function LinkFunction(
+ { to, children, variant, className, onClick },
ref
) {
- return external ? (
-
- {children}
-
- ) : (
+ return (
{children}
diff --git a/packages/ui-components/src/components/Link/__snapshots__/Link.test.tsx.snap b/packages/ui-components/src/components/Link/__snapshots__/Link.test.tsx.snap
new file mode 100644
index 000000000..ff44dec21
--- /dev/null
+++ b/packages/ui-components/src/components/Link/__snapshots__/Link.test.tsx.snap
@@ -0,0 +1,63 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` 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;
+}
+
+
+
+
+`;
+
+exports[` 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;
+}
+
+
+
+ Home
+
+
+`;
diff --git a/packages/ui-components/src/components/Link/index.ts b/packages/ui-components/src/components/Link/index.ts
index abdf73e27..241046084 100644
--- a/packages/ui-components/src/components/Link/index.ts
+++ b/packages/ui-components/src/components/Link/index.ts
@@ -1 +1 @@
-export { default as Link } from './Link';
+export { default } from './Link';
diff --git a/packages/ui-components/src/components/LinkExternal/LinkExternal.test.tsx b/packages/ui-components/src/components/LinkExternal/LinkExternal.test.tsx
new file mode 100644
index 000000000..550c82d2b
--- /dev/null
+++ b/packages/ui-components/src/components/LinkExternal/LinkExternal.test.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+import { render } from '../../test/test-react-testing-library';
+import LinkExternal from './LinkExternal';
+
+describe(' component', () => {
+ test('should render the component in default state', () => {
+ const { container } = render( );
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ test('should render the component with external link', () => {
+ const { container } = render(
+ {'Example'}
+ );
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/packages/ui-components/src/components/LinkExternal/LinkExternal.tsx b/packages/ui-components/src/components/LinkExternal/LinkExternal.tsx
new file mode 100644
index 000000000..84e6f72a4
--- /dev/null
+++ b/packages/ui-components/src/components/LinkExternal/LinkExternal.tsx
@@ -0,0 +1,22 @@
+import Link from '@mui/material/Link';
+import React from 'react';
+
+const LinkExternal = React.forwardRef((props, ref) => {
+ const { to, children, variant, ...rest } = props;
+ return (
+ // eslint-disable-next-line verdaccio/jsx-spread
+
+ {children}
+
+ );
+});
+
+export default LinkExternal;
diff --git a/packages/ui-components/src/components/LinkExternal/__snapshots__/LinkExternal.test.tsx.snap b/packages/ui-components/src/components/LinkExternal/__snapshots__/LinkExternal.test.tsx.snap
new file mode 100644
index 000000000..2904d23e8
--- /dev/null
+++ b/packages/ui-components/src/components/LinkExternal/__snapshots__/LinkExternal.test.tsx.snap
@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` 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;
+}
+
+
+`;
+
+exports[` 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;
+}
+
+
+ Example
+
+`;
diff --git a/packages/ui-components/src/components/LinkExternal/index.ts b/packages/ui-components/src/components/LinkExternal/index.ts
new file mode 100644
index 000000000..f69e6b899
--- /dev/null
+++ b/packages/ui-components/src/components/LinkExternal/index.ts
@@ -0,0 +1 @@
+export { default } from './LinkExternal';
diff --git a/packages/ui-components/src/components/Loading/Loading.test.tsx b/packages/ui-components/src/components/Loading/Loading.test.tsx
index d0096cdee..9f9ad3bc9 100644
--- a/packages/ui-components/src/components/Loading/Loading.test.tsx
+++ b/packages/ui-components/src/components/Loading/Loading.test.tsx
@@ -1,12 +1,15 @@
import React from 'react';
-import { render, screen } from '../../test/test-react-testing-library';
+import { act, render, screen, waitFor } from '../../test/test-react-testing-library';
import Loading from './Loading';
describe(' component', () => {
- test('should render the component in default state', () => {
- render( );
- screen.debug();
- expect(screen.getByTestId('loading')).toBeInTheDocument();
+ test('should render the component in default state', async () => {
+ act(() => {
+ render( );
+ });
+ await waitFor(() => {
+ expect(screen.getByTestId('loading')).toBeInTheDocument();
+ });
});
});
diff --git a/packages/ui-components/src/components/Loading/styles.ts b/packages/ui-components/src/components/Loading/styles.ts
index 8eac19cee..5701982f7 100644
--- a/packages/ui-components/src/components/Loading/styles.ts
+++ b/packages/ui-components/src/components/Loading/styles.ts
@@ -11,6 +11,7 @@ export const Wrapper = styled('div')({
export const Badge = styled('div')<{ theme?: Theme }>(({ theme }) => ({
margin: '0 0 30px 0',
+ padding: 5,
borderRadius: 25,
boxShadow: '0 10px 20px 0 rgba(69, 58, 100, 0.2)',
background: theme?.palette.mode === 'dark' ? theme?.palette.black : '#f7f8f6',
diff --git a/packages/ui-components/src/components/LoginDialog/LoginDialog.test.tsx b/packages/ui-components/src/components/LoginDialog/LoginDialog.test.tsx
index a8a2897fc..c3a5389dc 100644
--- a/packages/ui-components/src/components/LoginDialog/LoginDialog.test.tsx
+++ b/packages/ui-components/src/components/LoginDialog/LoginDialog.test.tsx
@@ -87,7 +87,7 @@ describe(' component', () => {
fireEvent.change(userNameInput, { target: { value: 'xyz' } });
const passwordInput = screen.getByPlaceholderText('form-placeholder.password');
- expect(userNameInput).toBeInTheDocument();
+ expect(passwordInput).toBeInTheDocument();
fireEvent.focus(passwordInput);
await act(async () => {
diff --git a/packages/ui-components/src/components/LoginDialog/LoginDialog.tsx b/packages/ui-components/src/components/LoginDialog/LoginDialog.tsx
index d26930760..116ef2e92 100644
--- a/packages/ui-components/src/components/LoginDialog/LoginDialog.tsx
+++ b/packages/ui-components/src/components/LoginDialog/LoginDialog.tsx
@@ -21,7 +21,7 @@ const LoginDialog: React.FC = ({ onClose, open = false }) => {
const makeLogin = useCallback(
async (username?: string, password?: string): Promise => {
// checks isEmpty
- if (isEmpty(username) || isEmpty(password)) {
+ if (!username || !password || isEmpty(username) || isEmpty(password)) {
dispatch.login.addError({
type: 'error',
description: i18next.t('form-validation.username-or-password-cant-be-empty'),
@@ -29,6 +29,15 @@ const LoginDialog: React.FC = ({ onClose, open = false }) => {
return;
}
+ // checks min username and password length
+ if (username.length < 2 || password.length < 2) {
+ dispatch.login.addError({
+ type: 'error',
+ description: i18next.t('form-validation.required-min-length', { length: 2 }),
+ });
+ return;
+ }
+
try {
dispatch.login.getUser({ username, password });
// const response: LoginBody = await doLogin(username as string, password as string);
diff --git a/packages/ui-components/src/components/LoginDialog/LoginDialogCloseButton.tsx b/packages/ui-components/src/components/LoginDialog/LoginDialogCloseButton.tsx
index 35302784b..f8a88ff5a 100644
--- a/packages/ui-components/src/components/LoginDialog/LoginDialogCloseButton.tsx
+++ b/packages/ui-components/src/components/LoginDialog/LoginDialogCloseButton.tsx
@@ -12,6 +12,7 @@ const StyledIconButton = styled(IconButton)<{ theme?: Theme }>(({ theme }) => ({
right: theme.spacing() / 2,
top: theme.spacing() / 2,
color: theme.palette.grey[500],
+ zIndex: 99,
}));
interface Props {
diff --git a/packages/ui-components/src/components/LoginDialog/LoginDialogForm.tsx b/packages/ui-components/src/components/LoginDialog/LoginDialogForm.tsx
index cead7126b..c443b154e 100644
--- a/packages/ui-components/src/components/LoginDialog/LoginDialogForm.tsx
+++ b/packages/ui-components/src/components/LoginDialog/LoginDialogForm.tsx
@@ -48,7 +48,6 @@ const LoginDialogForm = memo(({ onSubmit, error }: Props) => {
id="login--dialog-username"
{...register('username', {
required: { value: true, message: t('form-validation.required-field') },
- minLength: { value: 2, message: t('form-validation.required-min-length', { length: 2 }) },
})}
label={t('form.username')}
margin="normal"
@@ -65,7 +64,6 @@ const LoginDialogForm = memo(({ onSubmit, error }: Props) => {
id="login--dialog-password"
{...register('password', {
required: { value: true, message: t('form-validation.required-field') },
- minLength: { value: 2, message: t('form-validation.required-min-length', { length: 2 }) },
})}
data-testid="password"
label={t('form.password')}
diff --git a/packages/ui-components/src/components/LoginDialog/LoginDialogFormError.tsx b/packages/ui-components/src/components/LoginDialog/LoginDialogFormError.tsx
index 706954bed..fc63595b1 100644
--- a/packages/ui-components/src/components/LoginDialog/LoginDialogFormError.tsx
+++ b/packages/ui-components/src/components/LoginDialog/LoginDialogFormError.tsx
@@ -30,7 +30,7 @@ const LoginDialogFormError = memo(({ error }: Props) => {
return (
+
{error.description}
diff --git a/packages/ui-components/src/components/Logo/Logo.stories.tsx b/packages/ui-components/src/components/Logo/Logo.stories.tsx
new file mode 100644
index 000000000..e3a5ad4da
--- /dev/null
+++ b/packages/ui-components/src/components/Logo/Logo.stories.tsx
@@ -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 = () => (
+
+
+
+
+
+);
diff --git a/packages/ui-components/src/components/Logo/Logo.test.tsx b/packages/ui-components/src/components/Logo/Logo.test.tsx
index 303884246..369bbafaa 100644
--- a/packages/ui-components/src/components/Logo/Logo.test.tsx
+++ b/packages/ui-components/src/components/Logo/Logo.test.tsx
@@ -1,11 +1,18 @@
import React from 'react';
-import { render } from '../../test/test-react-testing-library';
+import { render, screen } from '../../test/test-react-testing-library';
import Logo from './Logo';
describe(' component', () => {
test('should render the component in default state', () => {
const { container } = render( );
expect(container.firstChild).toMatchSnapshot();
+ expect(screen.getByTestId('default-logo')).toBeTruthy();
+ });
+
+ test('should render custom logo', () => {
+ window.__VERDACCIO_BASENAME_UI_OPTIONS.logo = 'custom.png';
+ render( );
+ expect(screen.getByTestId('custom-logo')).toBeTruthy();
});
});
diff --git a/packages/ui-components/src/components/Logo/Logo.tsx b/packages/ui-components/src/components/Logo/Logo.tsx
index 89a1c9251..b664d2ee1 100644
--- a/packages/ui-components/src/components/Logo/Logo.tsx
+++ b/packages/ui-components/src/components/Logo/Logo.tsx
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
+import { useTheme } from '@mui/styles';
import React from 'react';
import { Theme, useConfig } from '../../';
@@ -22,18 +23,33 @@ interface Props {
onClick?: () => void;
className?: string;
isDefault?: boolean;
+ title?: string;
}
-const Logo: React.FC = ({ size, onClick, className, isDefault = false }) => {
+const Logo: React.FC = ({ size, onClick, className, isDefault = false, title = '' }) => {
const { configOptions } = useConfig();
+ const theme = useTheme();
if (!isDefault && configOptions?.logo) {
+ const logoSrc =
+ theme?.palette.mode === 'dark' && configOptions.logoDark
+ ? configOptions.logoDark
+ : configOptions.logo;
return (
-
+
);
}
- return ;
+
+ return (
+
+ );
};
export default Logo;
diff --git a/packages/ui-components/src/components/Logo/__snapshots__/Logo.test.tsx.snap b/packages/ui-components/src/components/Logo/__snapshots__/Logo.test.tsx.snap
index 2d1c349ba..8c0236605 100644
--- a/packages/ui-components/src/components/Logo/__snapshots__/Logo.test.tsx.snap
+++ b/packages/ui-components/src/components/Logo/__snapshots__/Logo.test.tsx.snap
@@ -17,5 +17,7 @@ exports[` component should render the component in default state 1`] = `
`;
diff --git a/packages/ui-components/src/components/Package/Package.test.tsx b/packages/ui-components/src/components/Package/Package.test.tsx
index c4598b7ce..c6e08e84d 100644
--- a/packages/ui-components/src/components/Package/Package.test.tsx
+++ b/packages/ui-components/src/components/Package/Package.test.tsx
@@ -2,7 +2,13 @@ import React from 'react';
import { MemoryRouter } from 'react-router';
import { store } from '../../';
-import { cleanup, renderWithStore } from '../../test/test-react-testing-library';
+import {
+ cleanup,
+ fireEvent,
+ renderWithStore,
+ screen,
+ waitFor,
+} from '../../test/test-react-testing-library';
import Package from './Package';
/**
@@ -11,29 +17,36 @@ import Package from './Package';
*/
const dateOneMonthAgo = (): Date => new Date(1544377770747);
+const props = {
+ name: 'verdaccio',
+ version: '1.0.0',
+ time: String(dateOneMonthAgo()),
+ license: 'MIT',
+ description: 'Private NPM repository',
+ author: {
+ name: 'Sam',
+ },
+ dist: {
+ fileCount: 1,
+ unpackedSize: 171,
+ tarball: 'http://localhost:9000/verdaccio/-/verdaccio-1.0.0.tgz',
+ },
+ keywords: ['verdaccio-core'],
+};
+
describe(' component', () => {
afterEach(() => {
cleanup();
});
test('should load the component', () => {
- const props = {
- name: 'verdaccio',
- version: '1.0.0',
- time: String(dateOneMonthAgo()),
- license: 'MIT',
- description: 'Private NPM repository',
- author: {
- name: 'Sam',
- },
- keywords: ['verdaccio'],
- };
-
const wrapper = renderWithStore(
component', () => {
expect(wrapper.getByText('MIT')).toBeInTheDocument();
});
- test.todo('should load the component without author');
+ // test if click on download button will trigger the download action
+ test('should download the package', async () => {
+ renderWithStore(
+
+
+ ,
+ store
+ );
+
+ fireEvent.click(screen.getByTestId('download-tarball'));
+ await waitFor(() => expect(store.getState().loading.models.download).toBe(true));
+ });
});
diff --git a/packages/ui-components/src/components/Package/Package.tsx b/packages/ui-components/src/components/Package/Package.tsx
index 9c51506f3..34d6df314 100644
--- a/packages/ui-components/src/components/Package/Package.tsx
+++ b/packages/ui-components/src/components/Package/Package.tsx
@@ -3,20 +3,20 @@ import styled from '@emotion/styled';
import BugReport from '@mui/icons-material/BugReport';
import DownloadIcon from '@mui/icons-material/CloudDownload';
import HomeIcon from '@mui/icons-material/Home';
-import { useTheme } from '@mui/material';
import CircularProgress from '@mui/material/CircularProgress';
import Grid from '@mui/material/Grid';
+import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import Tooltip from '@mui/material/Tooltip';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
-import { Dispatch, Link, RootState, Theme } from '../../';
+import { Dispatch, Link, LinkExternal, RootState, Theme } from '../../';
import { FileBinary, Law, Time, Version } from '../../components/Icons';
import { Author as PackageAuthor, PackageMetaInterface } from '../../types/packageMeta';
-import { url, utils } from '../../utils';
-import Tag from './Tag';
+import { Route, url, utils } from '../../utils';
+import KeywordListItems from '../Keywords/KeywordListItems';
import {
Author,
Avatar,
@@ -28,7 +28,6 @@ import {
PackageListItemText,
PackageTitle,
Published,
- TagContainer,
Wrapper,
WrapperLink,
} from './styles';
@@ -47,7 +46,7 @@ export interface PackageInterface {
time?: number | string;
author: PackageAuthor;
description?: string;
- keywords?: string[];
+ keywords?: PackageMetaInterface['latest']['keywords'];
license?: PackageMetaInterface['latest']['license'];
homepage?: string;
bugs?: Bugs;
@@ -61,7 +60,7 @@ const Package: React.FC = ({
description,
dist,
homepage,
- keywords = [],
+ keywords,
license,
name: packageName,
time,
@@ -71,7 +70,6 @@ const Package: React.FC = ({
const config = useSelector((state: RootState) => state.configuration.config);
const dispatch = useDispatch();
const { t } = useTranslation();
- const theme = useTheme();
const isLoading = useSelector((state: RootState) => state?.loading?.models.download);
const handleDownload = useCallback(
@@ -94,21 +92,12 @@ const Package: React.FC = ({
const renderAuthorInfo = (): React.ReactNode => {
const name = utils.getAuthorName(authorName);
return (
-
-
-
-
- {name}
-
-
-
+
+
+
+ {name}
+
+
);
};
@@ -132,33 +121,34 @@ const Package: React.FC = ({
time && (
- {t('package.published-on', { time: utils.formatDate(time) })}
- {utils.formatDateDistance(time)}
+
+ {t('package.published-on', { time: utils.formatDateDistance(time) })}
+
);
const renderHomePageLink = (): React.ReactNode =>
homepage &&
url.isURL(homepage) && (
-
+
-
+
);
const renderBugsLink = (): React.ReactNode =>
bugs?.url &&
url.isURL(bugs.url) && (
-
+
-
+
);
const renderDownloadLink = (): React.ReactNode =>
@@ -174,7 +164,11 @@ const Package: React.FC = ({
aria-label={t('package.download', { what: t('package.the-tar-file') })}
title={t('package.tarball')}
>
-
+
{isLoading ? (
@@ -191,7 +185,7 @@ const Package: React.FC = ({
return (
-
+
{packageName}
@@ -213,15 +207,7 @@ const Package: React.FC = ({
);
};
- const renderSecondaryComponent = (): React.ReactNode => {
- const tags = keywords.sort().map((keyword, index) => {keyword} );
- return (
- <>
- {description}
- {tags.length > 0 && {tags} }
- >
- );
- };
+ const renderSecondaryComponent = (): React.ReactNode => {description} ;
const renderPackageListItemText = (): React.ReactNode => (
= ({
/>
);
+ const renderKeywords = (): React.ReactNode => (
+
+
+
+
+
+ );
+
return (
- {renderPackageListItemText()}
-
+
+ {renderPackageListItemText()}
+
+ {keywords && keywords?.length > 0 ? renderKeywords() : null}
+
{renderAuthorInfo()}
{renderVersionInfo()}
{renderPublishedInfo()}
@@ -248,8 +245,8 @@ const Package: React.FC = ({
export default Package;
const iconStyle = ({ theme }: { theme: Theme }) => css`
- margin: 2px 10px 0 0;
- fill: ${theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white};
+ margin: 0 10px 0 0;
+ fill: ${theme?.palette.mode === 'light' ? theme?.palette.greyDark : theme?.palette.white};
`;
const StyledVersion = styled(Version)`
diff --git a/packages/ui-components/src/components/Package/Tag/Tag.test.tsx b/packages/ui-components/src/components/Package/Tag/Tag.test.tsx
deleted file mode 100644
index 3d420f2bc..000000000
--- a/packages/ui-components/src/components/Package/Tag/Tag.test.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-
-import { render } from '../../../test/test-react-testing-library';
-import Tag from './Tag';
-
-describe(' component', () => {
- test('should load the component in default state', () => {
- const { container } = render(
-
- {'I am a child'}
-
- );
- expect(container.firstChild).toMatchSnapshot();
- });
-});
diff --git a/packages/ui-components/src/components/Package/Tag/Tag.tsx b/packages/ui-components/src/components/Package/Tag/Tag.tsx
deleted file mode 100644
index f5944d28b..000000000
--- a/packages/ui-components/src/components/Package/Tag/Tag.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React, { ReactNode } from 'react';
-
-import { Wrapper } from './styles';
-
-interface Props {
- children: ReactNode;
-}
-
-const Tag: React.FC = ({ children }) => {children} ;
-
-export default Tag;
diff --git a/packages/ui-components/src/components/Package/Tag/__snapshots__/Tag.test.tsx.snap b/packages/ui-components/src/components/Package/Tag/__snapshots__/Tag.test.tsx.snap
deleted file mode 100644
index 820501603..000000000
--- a/packages/ui-components/src/components/Package/Tag/__snapshots__/Tag.test.tsx.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` 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;
-}
-
-
-
- I am a child
-
-
-`;
diff --git a/packages/ui-components/src/components/Package/Tag/index.ts b/packages/ui-components/src/components/Package/Tag/index.ts
deleted file mode 100644
index 15774bff2..000000000
--- a/packages/ui-components/src/components/Package/Tag/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './Tag';
diff --git a/packages/ui-components/src/components/Package/Tag/styles.ts b/packages/ui-components/src/components/Package/Tag/styles.ts
deleted file mode 100644
index 6ed1ef8aa..000000000
--- a/packages/ui-components/src/components/Package/Tag/styles.ts
+++ /dev/null
@@ -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',
-});
diff --git a/packages/ui-components/src/components/Package/styles.ts b/packages/ui-components/src/components/Package/styles.ts
index 00c98c5e5..5191d9c64 100644
--- a/packages/ui-components/src/components/Package/styles.ts
+++ b/packages/ui-components/src/components/Package/styles.ts
@@ -11,8 +11,8 @@ import { Theme } from '../../';
export const OverviewItem = styled('span')<{ theme?: Theme }>(({ theme }) => ({
display: 'flex',
alignItems: 'center',
- margin: '0 0 0 16px',
- color: theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white,
+ margin: '0 20px 0 0',
+ color: theme?.palette.mode === 'light' ? theme?.palette.greyDark2 : theme?.palette.white,
fontSize: 12,
[`@media (max-width: ${theme?.breakPoints.medium}px)`]: {
':nth-of-type(3)': {
@@ -26,10 +26,9 @@ export const OverviewItem = styled('span')<{ theme?: Theme }>(({ theme }) => ({
},
}));
-export const Published = styled('span')<{ theme?: Theme }>(({ theme }) => ({
- color: theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white,
+export const Published = styled('span')({
margin: '0 5px 0 0',
-}));
+});
export const Details = styled('span')({
marginLeft: '5px',
@@ -59,6 +58,7 @@ export const PackageTitle = styled('span')<{ theme?: Theme }>(({ theme }) => ({
marginBottom: 12,
color: theme?.palette.mode == 'dark' ? theme?.palette.dodgerBlue : theme?.palette.eclipse,
cursor: 'pointer',
+ textDecoration: 'none',
[`@media (max-width: ${theme?.breakPoints.small}px)`]: {
fontSize: 14,
marginBottom: 8,
@@ -70,9 +70,9 @@ export const GridRightAligned = styled(Grid)({
});
export const Wrapper = styled(List)<{ theme?: Theme }>(({ theme }) => ({
- ':hover': {
+ '&:hover': {
backgroundColor:
- theme?.palette?.type == 'dark' ? theme?.palette?.secondary.main : theme?.palette?.greyLight3,
+ theme?.palette?.type == 'dark' ? theme?.palette?.secondary.main : theme?.palette?.greyLight2,
},
}));
@@ -83,16 +83,6 @@ export const IconButton = styled(MuiIconButton)({
},
});
-export const TagContainer = styled('span')<{ theme?: Theme }>(({ theme }) => ({
- marginTop: 8,
- marginBottom: 12,
- display: 'flex',
- flexWrap: 'wrap',
- [`@media (max-width: ${theme?.breakPoints.medium}px)`]: {
- display: 'none',
- },
-}));
-
export const PackageListItemText = styled(ListItemText)({
paddingRight: 0,
});
diff --git a/packages/ui-components/src/components/PackageList/Packagelist.test.tsx b/packages/ui-components/src/components/PackageList/Packagelist.test.tsx
index 425cd3d8b..84b466e65 100644
--- a/packages/ui-components/src/components/PackageList/Packagelist.test.tsx
+++ b/packages/ui-components/src/components/PackageList/Packagelist.test.tsx
@@ -40,6 +40,7 @@ describe(' component', () => {
name: 'xyz',
version: '1.1.0',
description: 'xyz description',
+ keywords: ['hello', 'mars'],
author: { name: 'Martin', avatar: 'test avatar' },
},
],
diff --git a/packages/ui-components/src/components/Person/Person.test.tsx b/packages/ui-components/src/components/Person/Person.test.tsx
new file mode 100644
index 000000000..444549954
--- /dev/null
+++ b/packages/ui-components/src/components/Person/Person.test.tsx
@@ -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 (
+
+ );
+};
+
+describe('Person component', () => {
+ test('should render avatar', () => {
+ render( );
+ 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( );
+ const name = screen.getByText(mockPerson.name);
+ expect(name).toBeInTheDocument();
+ });
+
+ test('should not render name when withText is false', async () => {
+ render( );
+ // hover over the avatar
+ fireEvent.mouseEnter(screen.getByTestId(mockPerson.name));
+ // wait for the tooltip to appear
+ await screen.findByTestId(mockPerson.name + '-tooltip');
+ });
+});
diff --git a/packages/ui-components/src/components/Person/Person.tsx b/packages/ui-components/src/components/Person/Person.tsx
new file mode 100644
index 000000000..9305b468a
--- /dev/null
+++ b/packages/ui-components/src/components/Person/Person.tsx
@@ -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 = (
+
+ );
+
+ return (
+ <>
+ }
+ >
+ {link.length > 0 ? (
+ {avatarComponent}
+ ) : (
+ avatarComponent
+ )}
+
+ {withText && (
+
+ {getName(person.name)}
+
+ )}
+ >
+ );
+};
+
+export default Person;
diff --git a/packages/ui-components/src/components/Person/PersonTooltip.tsx b/packages/ui-components/src/components/Person/PersonTooltip.tsx
new file mode 100644
index 000000000..0164506e8
--- /dev/null
+++ b/packages/ui-components/src/components/Person/PersonTooltip.tsx
@@ -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 }) => (
+
+ {person.name}
+ {person.email && url.isEmail(person.email) && (
+
+
+ {person.email}
+
+ )}
+ {person.url && url.isURL(person.url) && (
+
+
+ {person.url}
+
+ )}
+
+);
+
+export default PersonTooltip;
diff --git a/packages/ui-components/src/components/Person/index.ts b/packages/ui-components/src/components/Person/index.ts
new file mode 100644
index 000000000..3f8744586
--- /dev/null
+++ b/packages/ui-components/src/components/Person/index.ts
@@ -0,0 +1 @@
+export { default } from './Person';
diff --git a/packages/ui-components/src/components/Person/utils.ts b/packages/ui-components/src/components/Person/utils.ts
new file mode 100644
index 000000000..9c067aae3
--- /dev/null
+++ b/packages/ui-components/src/components/Person/utils.ts
@@ -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;
+}
diff --git a/packages/ui-components/src/components/RawViewer/RawViewer.tsx b/packages/ui-components/src/components/RawViewer/RawViewer.tsx
index 3c2bb355b..9a7888711 100644
--- a/packages/ui-components/src/components/RawViewer/RawViewer.tsx
+++ b/packages/ui-components/src/components/RawViewer/RawViewer.tsx
@@ -3,6 +3,7 @@ import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import IconButton from '@mui/material/IconButton';
+import { useTheme } from '@mui/styles';
import React from 'react';
import { useTranslation } from 'react-i18next';
import ReactJson from 'react-json-view';
@@ -22,6 +23,7 @@ const ViewerTitle = (props: ViewerTitleProps) => {
{onClose ? (
= ({ isOpen = false, onClose, packageMeta }) => {
const { t } = useTranslation();
+ const theme = useTheme();
return (
- {t('action-bar-action.raw')}
+ {t('action-bar-action.raw-title', { package: packageMeta.latest.name })}
diff --git a/packages/ui-components/src/components/Readme/Readme.spec.tsx b/packages/ui-components/src/components/Readme/Readme.spec.tsx
index 8a981ee5f..abf8d935e 100644
--- a/packages/ui-components/src/components/Readme/Readme.spec.tsx
+++ b/packages/ui-components/src/components/Readme/Readme.spec.tsx
@@ -15,4 +15,21 @@ describe(' component', () => {
const wrapper = render( );
expect(wrapper.getByText('This is a test string')).toBeInTheDocument();
});
+
+ test('should sanitize html', () => {
+ const markdown = ``;
+ const wrapper = render( );
+ expect(wrapper.queryAllByText('test')).toHaveLength(0);
+ });
+
+ test('should highlight code', () => {
+ const markdown = `\`\`\`js\nconst test = 1 + 2;\n\`\`\``;
+ const wrapper = render( );
+ expect(wrapper.getByText('const')).toBeInTheDocument();
+ expect(wrapper.getByText('const')).toHaveClass('hljs-keyword');
+ expect(wrapper.getByText('1')).toBeInTheDocument();
+ expect(wrapper.getByText('1')).toHaveClass('hljs-number');
+ expect(wrapper.getByText('2')).toBeInTheDocument();
+ expect(wrapper.getByText('2')).toHaveClass('hljs-number');
+ });
});
diff --git a/packages/ui-components/src/components/Readme/Readme.tsx b/packages/ui-components/src/components/Readme/Readme.tsx
index f840864e5..2cceb6d75 100644
--- a/packages/ui-components/src/components/Readme/Readme.tsx
+++ b/packages/ui-components/src/components/Readme/Readme.tsx
@@ -2,8 +2,6 @@ import styled from '@emotion/styled';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
-import { useTheme } from '@mui/styles';
-import 'highlight.js/styles/default.css';
import React from 'react';
import { useCustomTheme } from '../../';
@@ -14,12 +12,19 @@ import { parseReadme } from './utils';
const Readme: React.FC = ({ description }) => {
// @ts-ignore
const { isDarkMode } = useCustomTheme();
- const theme = useTheme();
+
+ // Stackoverflow theme fits well to Verdaccio dark colors
+ // https://highlightjs.org/examples
+ if (isDarkMode) {
+ require('highlight.js/styles/github-dark.css');
+ } else {
+ require('highlight.js/styles/github.css');
+ }
return (
-
+
-
+
component should load the component in default state 1`] = `
box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12);
background-image: unset;
overflow: hidden;
+ margin-bottom: 16px;
}
.emotion-1 {
@@ -23,7 +24,7 @@ exports[` component should load the component in default state 1`] = `
}
.emotion-2 {
- margin: 8px;
+ margin: 16px;
}
.emotion-3 ul {
@@ -69,6 +70,7 @@ exports[` component should load the component in default state 1`] = `
box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12);
background-image: unset;
overflow: hidden;
+ margin-bottom: 16px;
}
.emotion-1 {
@@ -80,7 +82,7 @@ exports[` component should load the component in default state 1`] = `
}
.emotion-2 {
- margin: 8px;
+ margin: 16px;
}
.emotion-3 ul {
diff --git a/packages/ui-components/src/components/Readme/utils.ts b/packages/ui-components/src/components/Readme/utils.ts
index 6b3400fe4..58179fab9 100644
--- a/packages/ui-components/src/components/Readme/utils.ts
+++ b/packages/ui-components/src/components/Readme/utils.ts
@@ -21,9 +21,6 @@ marked.setOptions({
});
export function parseReadme(readme: string): string | void {
- if (typeof readme === 'string') {
- const html = marked.parse(readme);
- return DOMPurify.sanitize(html);
- }
- return '';
+ const html = marked.parse(readme);
+ return DOMPurify.sanitize(html);
}
diff --git a/packages/ui-components/src/components/RegistryInfoDialog/styles.ts b/packages/ui-components/src/components/RegistryInfoDialog/styles.ts
index d80dfc7e9..439da06e4 100644
--- a/packages/ui-components/src/components/RegistryInfoDialog/styles.ts
+++ b/packages/ui-components/src/components/RegistryInfoDialog/styles.ts
@@ -4,10 +4,11 @@ import DialogTitle from '@mui/material/DialogTitle';
import { Theme } from '../../';
-export const Title = styled(DialogTitle)<{ theme?: Theme }>((props) => ({
- backgroundColor: props.theme?.palette.primary.main,
- color: props.theme?.palette.white,
- fontSize: props.theme?.fontSize.lg,
+export const Title = styled(DialogTitle)<{ theme?: Theme }>(({ theme }) => ({
+ backgroundColor:
+ theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.cyanBlue,
+ color: theme?.palette.white,
+ fontSize: theme?.fontSize.lg,
}));
export const Content = styled(DialogContent)<{ theme?: Theme }>(({ theme }) => ({
diff --git a/packages/ui-components/src/components/Repository/Repository.tsx b/packages/ui-components/src/components/Repository/Repository.tsx
index fd96d4698..9aa9dc56a 100644
--- a/packages/ui-components/src/components/Repository/Repository.tsx
+++ b/packages/ui-components/src/components/Repository/Repository.tsx
@@ -4,6 +4,7 @@ import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
+import { useTheme } from '@mui/styles';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,20 +12,13 @@ import { Theme } from '../../Theme';
import { url as urlUtils } from '../../utils';
import CopyClipboard from '../CopyClipboard';
import { Git } from '../Icons';
-import { Link } from '../Link';
+import LinkExternal from '../LinkExternal';
const StyledText = styled(Typography)<{ theme?: Theme }>((props) => ({
fontWeight: props.theme?.fontWeight.bold,
textTransform: 'capitalize',
}));
-const GithubLink = styled(Link)<{ theme?: Theme }>(({ theme }) => ({
- color: theme?.palette.mode === 'light' ? theme?.palette.primary.main : theme?.palette.white,
- ':hover': {
- color: theme?.palette.dodgerBlue,
- },
-}));
-
const RepositoryListItem = styled(ListItem)({
padding: 0,
':hover': {
@@ -33,18 +27,19 @@ const RepositoryListItem = styled(ListItem)({
});
const RepositoryListItemText = styled(ListItemText)({
- padding: '0 10px',
+ padding: '0 0 0 10px',
margin: 0,
});
const RepositoryAvatar = styled(Avatar)({
- borderRadius: '0px',
- padding: '0',
+ padding: 0,
+ marginLeft: 0,
backgroundColor: 'transparent',
});
const Repository: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
const { t } = useTranslation();
+ const theme = useTheme();
const url = packageMeta?.latest?.repository?.url;
if (!url || !urlUtils.isURL(url)) {
return null;
@@ -66,15 +61,15 @@ const Repository: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
subheader={{t('sidebar.repository.title')} }
>
-
+
-
+
{repositoryURL}
-
+
}
/>
diff --git a/packages/ui-components/src/components/Search/AutoComplete/styles.ts b/packages/ui-components/src/components/Search/AutoComplete/styles.ts
new file mode 100644
index 000000000..7cae9fb90
--- /dev/null
+++ b/packages/ui-components/src/components/Search/AutoComplete/styles.ts
@@ -0,0 +1,7 @@
+import styled from '@emotion/styled';
+
+export const Wrapper = styled('div')({
+ width: '100%',
+ position: 'relative',
+ zIndex: 1,
+});
diff --git a/packages/ui-components/src/components/Search/AutoComplete/styles.tsx b/packages/ui-components/src/components/Search/AutoComplete/styles.tsx
deleted file mode 100644
index a22229b88..000000000
--- a/packages/ui-components/src/components/Search/AutoComplete/styles.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import styled from '@emotion/styled';
-
-import { TextField, Theme } from '../../../';
-
-export interface InputFieldProps {
- color: string;
-}
-
-export const Wrapper = styled('div')({
- width: '100%',
- position: 'relative',
- zIndex: 1,
-});
-
-export const StyledTextField = styled(TextField)<{ theme?: Theme }>((props) => ({
- '& .MuiInputBase-root': {
- ':before': {
- content: "''",
- border: 'none',
- },
- ':after': {
- borderColor: props.theme?.palette.white,
- },
- ':hover:before': {
- content: 'none',
- },
- ':hover:after': {
- content: 'none',
- transform: 'scaleX(1)',
- },
- [`@media screen and (min-width: ${props.theme?.breakPoints.medium}px)`]: {
- ':hover:after': {
- content: "''",
- },
- },
- },
- '& .MuiInputBase-input': {
- [`@media screen and (min-width: ${props.theme?.breakPoints.medium}px)`]: {
- color: props.theme?.palette.white,
- },
- },
-}));
diff --git a/packages/ui-components/src/components/Search/Search.test.tsx b/packages/ui-components/src/components/Search/Search.test.tsx
index fa4aef9ae..71242ae4c 100644
--- a/packages/ui-components/src/components/Search/Search.test.tsx
+++ b/packages/ui-components/src/components/Search/Search.test.tsx
@@ -4,6 +4,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
import { api, store } from '../../';
import { fireEvent, renderWithStore, screen, waitFor } from '../../test/test-react-testing-library';
import Search from './Search';
+import { cleanDescription } from './utils';
jest.mock('lodash/debounce', () =>
jest.fn((fn) => {
@@ -45,7 +46,7 @@ describe(' component', () => {
});
test('handleSearch: when user type package name in search component, show suggestions', async () => {
- const { getByPlaceholderText, getAllByText } = renderWithStore(
+ const { getByPlaceholderText, findAllByText } = renderWithStore(
,
store
);
@@ -57,14 +58,14 @@ describe(' component', () => {
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
- const suggestionsElements = await waitFor(() => getAllByText('verdaccio', { exact: true }));
+ const suggestionsElements = await waitFor(() => findAllByText('verdaccio', { exact: true }));
expect(suggestionsElements).toHaveLength(1);
expect(api.request).toHaveBeenCalledTimes(1);
});
test('onBlur: should cancel all search requests', async () => {
- const { getByPlaceholderText, getAllByText } = renderWithStore(
+ const { getByPlaceholderText, findAllByText } = renderWithStore(
,
store
);
@@ -75,7 +76,7 @@ describe(' component', () => {
fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
- const suggestionsElements = await waitFor(() => getAllByText('verdaccio', { exact: true }));
+ const suggestionsElements = await waitFor(() => findAllByText('verdaccio', { exact: true }));
expect(suggestionsElements).toHaveLength(1);
expect(api.request).toHaveBeenCalledTimes(1);
@@ -110,7 +111,7 @@ describe(' component', () => {
});
test('handlePackagesClearRequested: should clear suggestions', async () => {
- const { getByPlaceholderText, getAllByText } = renderWithStore(
+ const { getByPlaceholderText, findAllByText } = renderWithStore(
,
store
);
@@ -120,18 +121,18 @@ describe(' component', () => {
fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
- const suggestionsElements = await waitFor(() => getAllByText('verdaccio', { exact: true }));
+ const suggestionsElements = await waitFor(() => findAllByText('verdaccio', { exact: true }));
expect(suggestionsElements).toHaveLength(1);
fireEvent.change(autoCompleteInput, { target: { value: ' ' } });
const listBoxElement = screen.queryAllByRole('listbox');
- // // when the page redirects, the list box should be empty again
+ // when the page redirects, the list box should be empty again
expect(listBoxElement).toHaveLength(0);
expect(api.request).toHaveBeenCalledTimes(1);
});
test('handleClickSearch: should change the window location on click or return key', async () => {
- const { getByPlaceholderText, getAllByText } = renderWithStore(
+ const { getByPlaceholderText, findAllByText } = renderWithStore(
,
store
);
@@ -141,7 +142,7 @@ describe(' component', () => {
fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
- const suggestionsElements = await waitFor(() => getAllByText('verdaccio', { exact: true }));
+ const suggestionsElements = await waitFor(() => findAllByText('verdaccio', { exact: true }));
// console.log('suggestionsElements', suggestionsElements);
expect(suggestionsElements).toHaveLength(1);
// click on the second suggestion
@@ -151,3 +152,29 @@ describe(' component', () => {
expect(listBoxElement).toHaveLength(0);
});
});
+
+describe('cleanDescription', () => {
+ test('should return plain text', () => {
+ const description = 'Hello, Mars!';
+ const output = cleanDescription(description);
+ expect(output).toBe(description);
+ });
+
+ test('should remove html tags from description', () => {
+ const description = 'verdaccio ';
+ const output = cleanDescription(description);
+ expect(output).toBe('verdaccio');
+ });
+
+ test('should remove markdown links from description', () => {
+ const description = '[verdaccio](https://verdaccio.org)';
+ const output = cleanDescription(description);
+ expect(output).toBe('verdaccio');
+ });
+
+ test('should remove markdown links', () => {
+ const description = '[]';
+ const output = cleanDescription(description);
+ expect(output).toBe('NPM version');
+ });
+});
diff --git a/packages/ui-components/src/components/Search/Search.tsx b/packages/ui-components/src/components/Search/Search.tsx
index a3fcd3413..364fdc898 100644
--- a/packages/ui-components/src/components/Search/Search.tsx
+++ b/packages/ui-components/src/components/Search/Search.tsx
@@ -9,6 +9,7 @@ import { RouteComponentProps, withRouter } from 'react-router';
import { SearchResultWeb } from '@verdaccio/types';
import { Dispatch, RootState, useConfig } from '../../';
+import { Route } from '../../utils';
import AutoComplete from './AutoComplete';
import SearchItem from './SearchItem';
import { StyledInputAdornment, StyledTextField } from './styles';
@@ -27,6 +28,7 @@ const Search: React.FC = ({ history }) => {
const { suggestions } = useSelector((state: RootState) => state.search);
const isLoading = useSelector((state: RootState) => state?.loading?.models.search);
const dispatch = useDispatch();
+
/**
* Cancel all the requests which are in pending state.
*/
@@ -60,9 +62,9 @@ const Search: React.FC = ({ history }) => {
if (searchRemote) {
// TODO: check this part
// @ts-ignore
- history.push(`/-/web/detail/${value.package.name}`);
+ history.push(`${Route.DETAIL}${value.package.name}`);
} else {
- history.push(`/-/web/detail/${value.name}`);
+ history.push(`${Route.DETAIL}${value.name}`);
}
break;
}
diff --git a/packages/ui-components/src/components/Search/SearchItem.tsx b/packages/ui-components/src/components/Search/SearchItem.tsx
index 336b2e87e..5606122c2 100644
--- a/packages/ui-components/src/components/Search/SearchItem.tsx
+++ b/packages/ui-components/src/components/Search/SearchItem.tsx
@@ -9,6 +9,8 @@ import Stack from '@mui/material/Stack';
import React from 'react';
import { useTranslation } from 'react-i18next';
+import { cleanDescription } from './utils';
+
type SearchItemProps = {
name: string;
version?: string;
@@ -28,15 +30,23 @@ export const Description = styled('div')<{ theme?: Theme }>(({ theme }) => ({
display: 'none',
color: theme?.palette?.greyLight2,
lineHeight: '1.5rem',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ alignItems: 'center',
+ overflow: 'hidden',
+ paddingLeft: theme.spacing(),
+ fontSize: theme?.fontSize.ssm,
+ [`@media (min-width: ${theme?.breakPoints.medium}px)`]: {
+ display: 'block',
+ width: '300px',
+ },
[`@media (min-width: ${theme?.breakPoints.large}px)`]: {
display: 'block',
- whiteSpace: 'nowrap',
- textOverflow: 'ellipsis',
- width: '200px',
- alignItems: 'center',
- overflow: 'hidden',
- paddingLeft: theme.spacing(),
- fontSize: theme?.fontSize.ssm,
+ width: '500px',
+ },
+ [`@media (min-width: 1440px)`]: {
+ display: 'block',
+ width: '600px',
},
}));
@@ -68,11 +78,12 @@ const SearchItem: React.FC = ({
// no action assigned by default
};
return (
+ // eslint-disable-next-line verdaccio/jsx-no-style
{name}
- {description && {description} }
+ {description && {cleanDescription(description)} }
{version && {version} }
diff --git a/packages/ui-components/src/components/Search/utils.ts b/packages/ui-components/src/components/Search/utils.ts
new file mode 100644
index 000000000..d3ad1edf2
--- /dev/null
+++ b/packages/ui-components/src/components/Search/utils.ts
@@ -0,0 +1,17 @@
+function removeHtmlTags(input: string): string {
+ let previous;
+ do {
+ previous = input;
+ input = input.replace(/<[^>]*>?/gm, '');
+ } while (input !== previous);
+ return input;
+}
+
+export function cleanDescription(description: string): string {
+ let output = description;
+ // remove html tags from description (e.g. )
+ output = removeHtmlTags(output);
+ // remove markdown links from description (e.g. [link](url))
+ output = output.replace(/\(.*?\)/gm, '').replace(/(\[!?|\])/gm, '');
+ return output;
+}
diff --git a/packages/ui-components/src/components/SettingsMenu/SettingsMenu.test.tsx b/packages/ui-components/src/components/SettingsMenu/SettingsMenu.test.tsx
new file mode 100644
index 000000000..f93dbf55c
--- /dev/null
+++ b/packages/ui-components/src/components/SettingsMenu/SettingsMenu.test.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+
+import { fireEvent, render, screen } from '../../test/test-react-testing-library';
+import SettingsMenu from './SettingsMenu';
+
+describe(' ', () => {
+ test('should handle menu open and close', async () => {
+ render( );
+ const button = screen.getByRole('button');
+ fireEvent.click(button);
+ await screen.findByRole('menu');
+ // TODO onClose
+ });
+
+ test('should handle latest select', () => {
+ const { getByRole, getByText } = render( );
+ const button = getByRole('button');
+ fireEvent.click(button);
+ const menuItem = getByText('sidebar.installation.latest');
+ fireEvent.click(menuItem);
+ expect(getByText('sidebar.installation.latest')).toBeInTheDocument();
+ });
+
+ test('should handle global select', () => {
+ const { getByRole, getByText } = render( );
+ const button = getByRole('button');
+ fireEvent.click(button);
+ const menuItem = getByText('sidebar.installation.global');
+ fireEvent.click(menuItem);
+ expect(getByText('sidebar.installation.global')).toBeInTheDocument();
+ });
+
+ test('should handle yarn modern select', () => {
+ const { getByRole, getByText } = render( );
+ const button = getByRole('button');
+ fireEvent.click(button);
+ const menuItem = getByText('sidebar.installation.yarnModern');
+ fireEvent.click(menuItem);
+ expect(getByText('sidebar.installation.yarnModern')).toBeInTheDocument();
+ });
+});
diff --git a/packages/ui-components/src/components/SettingsMenu/SettingsMenu.tsx b/packages/ui-components/src/components/SettingsMenu/SettingsMenu.tsx
index 68a01a167..5aa3648c7 100644
--- a/packages/ui-components/src/components/SettingsMenu/SettingsMenu.tsx
+++ b/packages/ui-components/src/components/SettingsMenu/SettingsMenu.tsx
@@ -7,24 +7,42 @@ import MenuItem from '@mui/material/MenuItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
+import { useConfig } from '../../providers';
import { useSettings } from '../../providers/PersistenceSettingProvider';
interface Props {
packageName: string;
}
-const InstallListItem: React.FC = ({ packageName }) => {
+const SettingsMenu: React.FC = ({ packageName }) => {
const { t } = useTranslation();
const { localSettings, updateSettings } = useSettings();
+ const { configOptions } = useConfig();
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
const handleOpenMenu = (event: React.MouseEvent) => {
setAnchorEl(event.currentTarget);
};
+ const handleLatestSelect = () => {
+ updateSettings({
+ ...localSettings,
+ [packageName]: {
+ global: localSettings[packageName]?.global,
+ latest: !localSettings[packageName]?.latest,
+ },
+ });
+ setAnchorEl(null);
+ };
+
const handleGlobalSelect = () => {
- const statusGlobal = !localSettings[packageName]?.global;
- updateSettings({ ...localSettings, [packageName]: { global: statusGlobal } });
+ updateSettings({
+ ...localSettings,
+ [packageName]: {
+ global: !localSettings[packageName]?.global,
+ latest: localSettings[packageName]?.latest,
+ },
+ });
setAnchorEl(null);
};
@@ -38,6 +56,7 @@ const InstallListItem: React.FC = ({ packageName }) => {
setAnchorEl(null);
};
+ const statusLatest = localSettings[packageName]?.latest;
const statusGlobal = localSettings[packageName]?.global;
return (
<>
@@ -56,10 +75,27 @@ const InstallListItem: React.FC = ({ packageName }) => {
'aria-labelledby': 'basic-button',
}}
anchorEl={anchorEl}
+ anchorOrigin={{
+ vertical: 'bottom',
+ horizontal: 'right',
+ }}
id="basic-menu"
onClose={handleClose}
open={open}
+ transformOrigin={{
+ vertical: 'top',
+ horizontal: 'right',
+ }}
>
+
+ {' '}
+ {statusLatest === true ? (
+
+
+
+ ) : null}
+ {t('sidebar.installation.latest')}
+
{' '}
{statusGlobal === true ? (
@@ -69,18 +105,20 @@ const InstallListItem: React.FC = ({ packageName }) => {
) : null}
{t('sidebar.installation.global')}
-
- {' '}
- {localSettings?.yarnModern ? (
-
-
-
- ) : null}
- {t('sidebar.installation.yarnModern')}
-
+ {configOptions?.pkgManagers?.includes('yarn') && (
+
+ {' '}
+ {localSettings?.yarnModern ? (
+
+
+
+ ) : null}
+ {t('sidebar.installation.yarnModern')}
+
+ )}
>
);
};
-export default InstallListItem;
+export default SettingsMenu;
diff --git a/packages/ui-components/src/components/SideBarTitle/SideBarTitle.tsx b/packages/ui-components/src/components/SideBarTitle/SideBarTitle.tsx
index dee55354b..d587faf3c 100644
--- a/packages/ui-components/src/components/SideBarTitle/SideBarTitle.tsx
+++ b/packages/ui-components/src/components/SideBarTitle/SideBarTitle.tsx
@@ -20,11 +20,23 @@ interface Props {
time: string;
}
+const Icon = styled.div<{ theme?: Theme }>(({ theme }) => ({
+ marginLeft: theme?.spacing(1),
+}));
+
const ModuleJS: React.FC<{ module: ModuleType | void }> = ({ module }) => {
if (module === 'commonjs') {
- return ;
+ return (
+
+
+
+ );
} else if (module === 'module') {
- return ;
+ return (
+
+
+
+ );
} else {
return null;
}
@@ -46,7 +58,11 @@ const DetailSidebarTitle: React.FC = ({
<>
{packageName}
- {hasTypes && }
+ {hasTypes && (
+
+
+
+ )}
>
diff --git a/packages/ui-components/src/components/UpLinks/UpLinks.test.tsx b/packages/ui-components/src/components/UpLinks/UpLinks.test.tsx
index 965c3c936..e6b656073 100644
--- a/packages/ui-components/src/components/UpLinks/UpLinks.test.tsx
+++ b/packages/ui-components/src/components/UpLinks/UpLinks.test.tsx
@@ -44,4 +44,11 @@ describe(' component', () => {
expect(wrapper).toMatchSnapshot();
});
+
+ test('should not render if input is missing', () => {
+ const wrapper = render( );
+ // expect nothing to be rendered
+ expect(wrapper.queryByTestId('no-uplinks-npm')).toBeNull();
+ expect(wrapper.queryByTestId('uplinks')).toBeNull();
+ });
});
diff --git a/packages/ui-components/src/components/UpLinks/UpLinks.tsx b/packages/ui-components/src/components/UpLinks/UpLinks.tsx
index 6713cf93e..56e34586c 100644
--- a/packages/ui-components/src/components/UpLinks/UpLinks.tsx
+++ b/packages/ui-components/src/components/UpLinks/UpLinks.tsx
@@ -1,3 +1,6 @@
+import Box from '@mui/material/Box';
+import Card from '@mui/material/Card';
+import CardContent from '@mui/material/CardContent';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import React from 'react';
@@ -5,6 +8,7 @@ import { useTranslation } from 'react-i18next';
import { utils } from '../../utils';
import NoItems from '../NoItems';
+import UpLinkLink from './UplinkLink';
import { ListItemText, Spacer, StyledText } from './styles';
const UpLinks: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
@@ -17,24 +21,43 @@ const UpLinks: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
const { _uplinks: uplinks, latest } = packageMeta;
if (Object.keys(uplinks).length === 0) {
- return ;
+ return (
+
+
+
+
+
+ );
}
return (
- <>
- {t('uplinks.title')}
-
- {Object.keys(uplinks)
- .reverse()
- .map((name) => (
-
- {name}
-
- {utils.formatDateDistance(uplinks[name].fetched)}
-
- ))}
-
- >
+
+
+
+ {t('uplinks.title')}
+
+ {Object.keys(uplinks)
+ .reverse()
+ .map((name) => (
+
+
+
+
+
+
+ {utils.formatDateDistance(uplinks[name].fetched)}
+
+
+ ))}
+
+
+
+
);
};
diff --git a/packages/ui-components/src/components/UpLinks/UplinkLink.tsx b/packages/ui-components/src/components/UpLinks/UplinkLink.tsx
new file mode 100644
index 000000000..de0c907dc
--- /dev/null
+++ b/packages/ui-components/src/components/UpLinks/UplinkLink.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import { utils } from '../../utils';
+import LinkExternal from '../LinkExternal';
+
+const UpLinkLink: React.FC<{ packageName: string; uplinkName: string }> = ({
+ packageName,
+ uplinkName,
+}) => {
+ const link = utils.getUplink(uplinkName, packageName);
+ return link ? (
+
+ {uplinkName}
+
+ ) : (
+ <>{uplinkName}>
+ );
+};
+
+export default UpLinkLink;
diff --git a/packages/ui-components/src/components/UpLinks/__snapshots__/UpLinks.test.tsx.snap b/packages/ui-components/src/components/UpLinks/__snapshots__/UpLinks.test.tsx.snap
index a253e695e..bea90626c 100644
--- a/packages/ui-components/src/components/UpLinks/__snapshots__/UpLinks.test.tsx.snap
+++ b/packages/ui-components/src/components/UpLinks/__snapshots__/UpLinks.test.tsx.snap
@@ -9,102 +9,21 @@ exports[` component should render the component when there is no upli
-webkit-transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
border-radius: 4px;
- box-shadow: none;
+ box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12);
background-image: unset;
- font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
- font-weight: 400;
- font-size: 0.875rem;
- line-height: 1.43;
- background-color: rgb(229, 246, 253);
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- padding: 6px 16px;
- color: rgb(1, 67, 97);
-}
-
-.emotion-0 .MuiAlert-icon {
- color: #0288d1;
+ overflow: hidden;
+ margin-bottom: 16px;
}
.emotion-1 {
- margin-right: 12px;
- padding: 7px 0;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- font-size: 22px;
- opacity: 0.9;
+ padding: 16px;
+}
+
+.emotion-1:last-child {
+ padding-bottom: 24px;
}
.emotion-2 {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- width: 1em;
- height: 1em;
- display: inline-block;
- fill: currentColor;
- -webkit-flex-shrink: 0;
- -ms-flex-negative: 0;
- flex-shrink: 0;
- -webkit-transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
- transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
- font-size: inherit;
-}
-
-.emotion-3 {
- padding: 8px 0;
- min-width: 0;
- overflow: auto;
-}
-
-.emotion-4 {
- margin: 0;
- font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
- font-weight: 400;
- font-size: 1rem;
- line-height: 1.5;
-}
-
-
-
- ,
- "container": .emotion-0 {
background-color: #fff;
color: rgba(0, 0, 0, 0.87);
-webkit-transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
@@ -125,11 +44,11 @@ exports[` component should render the component when there is no upli
color: rgb(1, 67, 97);
}
-.emotion-0 .MuiAlert-icon {
+.emotion-2 .MuiAlert-icon {
color: #0288d1;
}
-.emotion-1 {
+.emotion-3 {
margin-right: 12px;
padding: 7px 0;
display: -webkit-box;
@@ -140,7 +59,7 @@ exports[` component should render the component when there is no upli
opacity: 0.9;
}
-.emotion-2 {
+.emotion-4 {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -157,13 +76,142 @@ exports[` component should render the component when there is no upli
font-size: inherit;
}
-.emotion-3 {
+.emotion-5 {
padding: 8px 0;
min-width: 0;
overflow: auto;
}
+.emotion-6 {
+ margin: 0;
+ font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
+ font-weight: 400;
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+
+
+ ,
+ "container": .emotion-0 {
+ background-color: #fff;
+ color: rgba(0, 0, 0, 0.87);
+ -webkit-transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ border-radius: 4px;
+ box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12);
+ background-image: unset;
+ overflow: hidden;
+ margin-bottom: 16px;
+}
+
+.emotion-1 {
+ padding: 16px;
+}
+
+.emotion-1:last-child {
+ padding-bottom: 24px;
+}
+
+.emotion-2 {
+ background-color: #fff;
+ color: rgba(0, 0, 0, 0.87);
+ -webkit-transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ border-radius: 4px;
+ box-shadow: none;
+ background-image: unset;
+ font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
+ font-weight: 400;
+ font-size: 0.875rem;
+ line-height: 1.43;
+ background-color: rgb(229, 246, 253);
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ padding: 6px 16px;
+ color: rgb(1, 67, 97);
+}
+
+.emotion-2 .MuiAlert-icon {
+ color: #0288d1;
+}
+
+.emotion-3 {
+ margin-right: 12px;
+ padding: 7px 0;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ font-size: 22px;
+ opacity: 0.9;
+}
+
.emotion-4 {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ width: 1em;
+ height: 1em;
+ display: inline-block;
+ fill: currentColor;
+ -webkit-flex-shrink: 0;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ -webkit-transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ font-size: inherit;
+}
+
+.emotion-5 {
+ padding: 8px 0;
+ min-width: 0;
+ overflow: auto;
+}
+
+.emotion-6 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
@@ -173,33 +221,41 @@ exports[` component should render the component when there is no upli
-
-
- uplinks.no-items
-
+
+
+
,
@@ -260,7 +316,31 @@ exports[` component should render the component when there is no upli
exports[` component should render the component with uplinks 1`] = `
{
"asFragment": [Function],
- "baseElement": .emotion-1 {
+ "baseElement": .emotion-0 {
+ background-color: #fff;
+ color: rgba(0, 0, 0, 0.87);
+ -webkit-transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ border-radius: 4px;
+ box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12);
+ background-image: unset;
+ overflow: hidden;
+ margin-bottom: 16px;
+}
+
+.emotion-1 {
+ padding: 16px;
+}
+
+.emotion-1:last-child {
+ padding-bottom: 24px;
+}
+
+.emotion-2 {
+ margin: 16px;
+}
+
+.emotion-4 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
@@ -269,7 +349,7 @@ exports[` component should render the component with uplinks 1`] = `
font-weight: 700;
}
-.emotion-2 {
+.emotion-5 {
list-style: none;
margin: 0;
padding: 0;
@@ -278,7 +358,7 @@ exports[` component should render the component with uplinks 1`] = `
padding-bottom: 8px;
}
-.emotion-3 {
+.emotion-6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@@ -297,29 +377,30 @@ exports[` component should render the component with uplinks 1`] = `
width: 100%;
box-sizing: border-box;
text-align: left;
- padding-top: 8px;
- padding-bottom: 8px;
+ padding-top: 4px;
+ padding-bottom: 4px;
padding-left: 16px;
padding-right: 16px;
+ padding-right: 0px;
}
-.emotion-3.Mui-focusVisible {
+.emotion-6.Mui-focusVisible {
background-color: rgba(0, 0, 0, 0.12);
}
-.emotion-3.Mui-selected {
+.emotion-6.Mui-selected {
background-color: rgba(75, 94, 64, 0.08);
}
-.emotion-3.Mui-selected.Mui-focusVisible {
+.emotion-6.Mui-selected.Mui-focusVisible {
background-color: rgba(75, 94, 64, 0.2);
}
-.emotion-3.Mui-disabled {
+.emotion-6.Mui-disabled {
opacity: 0.38;
}
-.emotion-5 {
+.emotion-8 {
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
@@ -333,63 +414,122 @@ exports[` component should render the component with uplinks 1`] = `
opacity: 0.6;
}
-.emotion-6 {
+.emotion-9 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
- font-size: 1rem;
- line-height: 1.5;
+ font-size: 0.875rem;
+ line-height: 1.43;
display: block;
}
-.emotion-7 {
+.emotion-10 {
+ margin: 0;
+ color: #4b5e40;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-10:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-11 {
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
border-bottom: 1px dotted rgba(0, 0, 0, 0.2);
white-space: nowrap;
height: 0.5em;
+ margin: 0 16px;
}
-
- uplinks.title
-
-
-
-
- npmjs
-
-
-
-
-
+
+
+
+
+
+
+ 6 years ago
+
+
+
+
-
-
+
+
,
- "container": .emotion-1 {
+ "container": .emotion-0 {
+ background-color: #fff;
+ color: rgba(0, 0, 0, 0.87);
+ -webkit-transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ border-radius: 4px;
+ box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12);
+ background-image: unset;
+ overflow: hidden;
+ margin-bottom: 16px;
+}
+
+.emotion-1 {
+ padding: 16px;
+}
+
+.emotion-1:last-child {
+ padding-bottom: 24px;
+}
+
+.emotion-2 {
+ margin: 16px;
+}
+
+.emotion-4 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
@@ -398,7 +538,7 @@ exports[` component should render the component with uplinks 1`] = `
font-weight: 700;
}
-.emotion-2 {
+.emotion-5 {
list-style: none;
margin: 0;
padding: 0;
@@ -407,7 +547,7 @@ exports[` component should render the component with uplinks 1`] = `
padding-bottom: 8px;
}
-.emotion-3 {
+.emotion-6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@@ -426,29 +566,30 @@ exports[` component should render the component with uplinks 1`] = `
width: 100%;
box-sizing: border-box;
text-align: left;
- padding-top: 8px;
- padding-bottom: 8px;
+ padding-top: 4px;
+ padding-bottom: 4px;
padding-left: 16px;
padding-right: 16px;
+ padding-right: 0px;
}
-.emotion-3.Mui-focusVisible {
+.emotion-6.Mui-focusVisible {
background-color: rgba(0, 0, 0, 0.12);
}
-.emotion-3.Mui-selected {
+.emotion-6.Mui-selected {
background-color: rgba(75, 94, 64, 0.08);
}
-.emotion-3.Mui-selected.Mui-focusVisible {
+.emotion-6.Mui-selected.Mui-focusVisible {
background-color: rgba(75, 94, 64, 0.2);
}
-.emotion-3.Mui-disabled {
+.emotion-6.Mui-disabled {
opacity: 0.38;
}
-.emotion-5 {
+.emotion-8 {
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
@@ -462,59 +603,94 @@ exports[` component should render the component with uplinks 1`] = `
opacity: 0.6;
}
-.emotion-6 {
+.emotion-9 {
margin: 0;
font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
font-weight: 400;
- font-size: 1rem;
- line-height: 1.5;
+ font-size: 0.875rem;
+ line-height: 1.43;
display: block;
}
-.emotion-7 {
+.emotion-10 {
+ margin: 0;
+ color: #4b5e40;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.emotion-10:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-11 {
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
border-bottom: 1px dotted rgba(0, 0, 0, 0.2);
white-space: nowrap;
height: 0.5em;
+ margin: 0 16px;
}
-
- uplinks.title
-
-
-
-
- npmjs
-
-
-
-
-
+
+
+
+
+
+
+ 6 years ago
+
+
+
+
-
-
+
+
,
"debug": [Function],
"findAllByAltText": [Function],
diff --git a/packages/ui-components/src/components/UpLinks/styles.ts b/packages/ui-components/src/components/UpLinks/styles.ts
index 874a7be97..96f03b3f8 100644
--- a/packages/ui-components/src/components/UpLinks/styles.ts
+++ b/packages/ui-components/src/components/UpLinks/styles.ts
@@ -15,6 +15,7 @@ export const Spacer = styled('div')<{ theme?: Theme }>(({ theme }) => ({
} `,
whiteSpace: 'nowrap',
height: '0.5em',
+ margin: '0 16px',
}));
export const ListItemText = styled(MuiListItemText)<{ theme?: Theme }>(({ theme }) => ({
diff --git a/packages/ui-components/src/components/Versions/HistoryList.tsx b/packages/ui-components/src/components/Versions/HistoryList.tsx
index 4792c736e..5cbcf9fe8 100644
--- a/packages/ui-components/src/components/Versions/HistoryList.tsx
+++ b/packages/ui-components/src/components/Versions/HistoryList.tsx
@@ -7,8 +7,8 @@ import { useTranslation } from 'react-i18next';
import { useConfig } from '../../providers';
import { Time, Versions } from '../../types/packageMeta';
-import { utils } from '../../utils';
-import { Link } from '../Link';
+import { Route, utils } from '../../utils';
+import Link from '../Link';
import { ListItemText, Spacer } from './styles';
interface Props {
@@ -40,8 +40,13 @@ const VersionsHistoryList: React.FC = ({ versions, packageName, time }) =
{Object.keys(listVersions)
.reverse()
.map((version) => (
-
-
+
+
{typeof versions[version]?.deprecated === 'string' ? (
diff --git a/packages/ui-components/src/components/Versions/TagList.tsx b/packages/ui-components/src/components/Versions/TagList.tsx
index bafe40143..cf065f7c0 100644
--- a/packages/ui-components/src/components/Versions/TagList.tsx
+++ b/packages/ui-components/src/components/Versions/TagList.tsx
@@ -3,7 +3,8 @@ import ListItem from '@mui/material/ListItem';
import React from 'react';
import { DistTags, Time } from '../../types/packageMeta';
-import { Link } from '../Link';
+import { Route } from '../../utils';
+import Link from '../Link';
import { ListItemText, Spacer } from './styles';
interface Props {
@@ -19,8 +20,8 @@ const VersionsTagList: React.FC = ({ tags, packageName, time }) => (
return time[tags[a]] < time[tags[b]] ? 1 : time[tags[a]] > time[tags[b]] ? -1 : 0;
})
.map((tag) => (
-
-
+
+
{tag}
diff --git a/packages/ui-components/src/components/Versions/Versions.test.tsx b/packages/ui-components/src/components/Versions/Versions.test.tsx
index fc16cf40b..40a9302a5 100644
--- a/packages/ui-components/src/components/Versions/Versions.test.tsx
+++ b/packages/ui-components/src/components/Versions/Versions.test.tsx
@@ -45,6 +45,8 @@ describe(' component', () => {
expect(screen.queryAllByTestId('version-list-text')).toHaveLength(65);
fireEvent.change(screen.getByRole('textbox'), { target: { value: '2.3.0' } });
expect(screen.queryAllByTestId('version-list-text')).toHaveLength(1);
+ fireEvent.change(screen.getByRole('textbox'), { target: { value: '' } });
+ expect(screen.queryAllByTestId('version-list-text')).toHaveLength(65);
});
test('should not render versions', () => {
diff --git a/packages/ui-components/src/components/Versions/Versions.tsx b/packages/ui-components/src/components/Versions/Versions.tsx
index c02140e8c..fcca52471 100644
--- a/packages/ui-components/src/components/Versions/Versions.tsx
+++ b/packages/ui-components/src/components/Versions/Versions.tsx
@@ -1,4 +1,8 @@
+import styled from '@emotion/styled';
import Alert from '@mui/material/Alert';
+import Box from '@mui/material/Box';
+import Card from '@mui/material/Card';
+import CardContent from '@mui/material/CardContent';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { useTheme } from '@mui/styles';
@@ -7,12 +11,18 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import semver from 'semver';
+import { Theme } from '../../Theme';
import { useConfig } from '../../providers';
import VersionsHistoryList from './HistoryList';
import VersionsTagList from './TagList';
export type Props = { packageMeta: any; packageName: string };
+export const StyledText = styled(Typography)<{ theme?: Theme }>((props) => ({
+ fontWeight: props.theme && props.theme.fontWeight.bold,
+ textTransform: 'capitalize',
+}));
+
const Versions: React.FC = ({ packageMeta, packageName }) => {
const { t } = useTranslation();
const { configOptions } = useConfig();
@@ -46,38 +56,53 @@ const Versions: React.FC = ({ packageMeta, packageName }) => {
};
return (
- <>
- {hasDistTags ? (
- <>
- {t('versions.current-tags')}
-
- >
- ) : null}
- <>
- {t('versions.version-history')}
- {
- filterVersions(e.target.value);
- }, 200)}
- size="small"
- variant="standard"
- />
- >
- {hasVersionHistory ? (
- <>
- {hideDeprecatedVersions === true && (
-
- {t('versions.hide-deprecated')}
-
- )}
-
- >
- ) : null}
- >
+
+
+
+ {hasDistTags ? (
+ <>
+
+ {t('versions.current-tags')}
+ {` (${Object.keys(distTags).length})`}
+
+
+ >
+ ) : null}
+ <>
+
+ {t('versions.version-history')}
+ {` (${Object.keys(packageVersions).length})`}
+
+ {
+ filterVersions(e.target.value);
+ }, 200)}
+ size="small"
+ variant="standard"
+ width="50%"
+ />
+ >
+ {hasVersionHistory ? (
+ <>
+ {hideDeprecatedVersions === true && (
+
+ {t('versions.hide-deprecated')}
+
+ )}
+
+ >
+ ) : null}
+
+
+
);
};
diff --git a/packages/ui-components/src/index.ts b/packages/ui-components/src/index.ts
index d113a369b..04ba004cb 100644
--- a/packages/ui-components/src/index.ts
+++ b/packages/ui-components/src/index.ts
@@ -1,6 +1,5 @@
export { default as CopyClipboard, copyToClipBoardUtility } from './components/CopyClipboard';
export * from './components/CopyClipboard';
-export { Link } from './components/Link';
export { default as ActionBar } from './components/ActionBar';
export { default as Author } from './components/Author';
export { default as Dependencies } from './components/Dependencies';
@@ -12,7 +11,11 @@ export { default as FundButton } from './components/FundButton';
export { default as Heading } from './components/Heading';
export * as Icons from './components/Icons';
export { default as Install } from './components/Install';
+export { default as Keywords } from './components/Keywords';
+export { default as Link } from './components/Link';
+export { default as LinkExternal } from './components/LinkExternal';
export { default as RawViewer } from './components/RawViewer';
+export { default as Person } from './components/Person';
export { default as Readme } from './components/Readme';
export { default as SideBarTitle } from './components/SideBarTitle';
export { default as UpLinks } from './components/UpLinks';
diff --git a/packages/ui-components/src/providers/AppConfigurationProvider/AppConfigurationProvider.tsx b/packages/ui-components/src/providers/AppConfigurationProvider/AppConfigurationProvider.tsx
index a591c0740..01f0396a2 100644
--- a/packages/ui-components/src/providers/AppConfigurationProvider/AppConfigurationProvider.tsx
+++ b/packages/ui-components/src/providers/AppConfigurationProvider/AppConfigurationProvider.tsx
@@ -28,6 +28,7 @@ const defaultValues: ConfigProviderProps = {
showSearch: true,
showRaw: true,
showDownloadTarball: true,
+ showUplinks: true,
hideDeprecatedVersions: false,
title: 'Verdaccio',
},
diff --git a/packages/ui-components/src/providers/PersistenceSettingProvider/PersistenceSettingProvider.tsx b/packages/ui-components/src/providers/PersistenceSettingProvider/PersistenceSettingProvider.tsx
index d3243022d..05da45eef 100644
--- a/packages/ui-components/src/providers/PersistenceSettingProvider/PersistenceSettingProvider.tsx
+++ b/packages/ui-components/src/providers/PersistenceSettingProvider/PersistenceSettingProvider.tsx
@@ -10,11 +10,13 @@ import React, {
import useLocalStorage from '../../hooks/useLocalStorage';
type PersistenceSettingsProps = {
+ isLatest?: boolean;
isGlobal?: boolean;
yarnModern: boolean;
};
const defaultValues: PersistenceSettingsProps = {
+ isLatest: false,
isGlobal: false,
yarnModern: false,
};
diff --git a/packages/ui-components/src/providers/VersionProvider/VersionProvider.test.tsx b/packages/ui-components/src/providers/VersionProvider/VersionProvider.test.tsx
index 73d76892d..a501a47e2 100644
--- a/packages/ui-components/src/providers/VersionProvider/VersionProvider.test.tsx
+++ b/packages/ui-components/src/providers/VersionProvider/VersionProvider.test.tsx
@@ -30,7 +30,7 @@ describe(' component with logged in state', () => {
});
test('should load data from the provider', async () => {
- await act(async () => {
+ act(() =>
renderWithStore(
@@ -40,8 +40,8 @@ describe(' component with logged in state', () => {
,
store
- );
- });
+ )
+ );
await waitFor(() => screen.getByText('storybook'));
expect(screen.getByText('storybook')).toBeInTheDocument();
expect(screen.getByText('MIT')).toBeInTheDocument();
diff --git a/packages/ui-components/src/sections/Detail/Detail.test.tsx b/packages/ui-components/src/sections/Detail/Detail.test.tsx
index c9e886341..d854568c7 100644
--- a/packages/ui-components/src/sections/Detail/Detail.test.tsx
+++ b/packages/ui-components/src/sections/Detail/Detail.test.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { render } from '../../test/test-react-testing-library';
+import { render, screen } from '../../test/test-react-testing-library';
import DetailContainer from './Detail';
describe('DetailContainer', () => {
@@ -8,5 +8,12 @@ describe('DetailContainer', () => {
const { container } = render( );
expect(container.firstChild).toMatchSnapshot();
});
+
+ test('renders without uplinks', () => {
+ window.__VERDACCIO_BASENAME_UI_OPTIONS.showUplinks = false;
+ render( );
+ expect(screen.queryByTestId('uplinks-tab')).toBeFalsy();
+ });
+
test.todo('should test click on tabs');
});
diff --git a/packages/ui-components/src/sections/Detail/Detail.tsx b/packages/ui-components/src/sections/Detail/Detail.tsx
index 407b64255..47e589f17 100644
--- a/packages/ui-components/src/sections/Detail/Detail.tsx
+++ b/packages/ui-components/src/sections/Detail/Detail.tsx
@@ -2,7 +2,7 @@ import Box from '@mui/material/Box';
import React, { useState } from 'react';
import Deprecated from '../../components/Deprecated';
-import { useVersion } from '../../providers';
+import { useConfig, useVersion } from '../../providers';
import ContainerContent from './ContainerContent';
import Tabs from './Tabs';
@@ -14,7 +14,10 @@ export enum TabPosition {
}
const DetailContainer: React.FC = () => {
- const tabs = Object.values(TabPosition);
+ const { configOptions } = useConfig();
+ const tabs = configOptions.showUplinks
+ ? Object.values(TabPosition)
+ : Object.values(TabPosition).filter((tab) => tab !== TabPosition.UPLINKS);
const [tabPosition, setTabPosition] = useState(0);
const { readMe, packageMeta } = useVersion();
@@ -23,8 +26,12 @@ const DetailContainer: React.FC = () => {
};
return (
-
-
+
+
{packageMeta?.latest?.deprecated && }
diff --git a/packages/ui-components/src/sections/Detail/Tabs.tsx b/packages/ui-components/src/sections/Detail/Tabs.tsx
index ffe3672f5..2edb6e5b2 100644
--- a/packages/ui-components/src/sections/Detail/Tabs.tsx
+++ b/packages/ui-components/src/sections/Detail/Tabs.tsx
@@ -9,16 +9,19 @@ import { Theme } from '../../Theme';
interface Props {
onChange: (event: React.ChangeEvent<{}>, newValue: number) => void;
tabPosition: number;
+ showUplinks?: boolean;
}
-const DetailContainerTabs: React.FC = ({ tabPosition, onChange }) => {
+const DetailContainerTabs: React.FC = ({ tabPosition, onChange, showUplinks }) => {
const { t } = useTranslation();
return (
-
+ {showUplinks && (
+
+ )}
);
};
diff --git a/packages/ui-components/src/sections/Detail/__snapshots__/Detail.test.tsx.snap b/packages/ui-components/src/sections/Detail/__snapshots__/Detail.test.tsx.snap
index 1a70b7291..07ee59f2a 100644
--- a/packages/ui-components/src/sections/Detail/__snapshots__/Detail.test.tsx.snap
+++ b/packages/ui-components/src/sections/Detail/__snapshots__/Detail.test.tsx.snap
@@ -9,7 +9,7 @@ exports[`DetailContainer renders correctly 1`] = `
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
- padding: 16px;
+ padding: 0px;
}
.emotion-2 {
diff --git a/packages/ui-components/src/sections/Footer/Footer.tsx b/packages/ui-components/src/sections/Footer/Footer.tsx
index 4ad6aa8c3..fd95da2c6 100644
--- a/packages/ui-components/src/sections/Footer/Footer.tsx
+++ b/packages/ui-components/src/sections/Footer/Footer.tsx
@@ -50,15 +50,22 @@ const Footer = () => {
+
+
+
{configOptions?.version && (
<>
- {t('footer.powered-by')}
-
- {`/ ${configOptions.version}`}
+ {t('footer.powered-by')}
+
>
)}
@@ -69,13 +76,17 @@ const Footer = () => {
export default Footer;
+const PoweredBy = styled('span')(() => ({
+ paddingRight: '5px',
+}));
+
const StyledEarth = styled(Earth)<{ theme?: Theme }>(({ theme }) => ({
margin: theme.spacing(0, 1),
}));
const Flags = styled('span')<{ theme?: Theme }>(({ theme }) => ({
display: 'inline-grid',
- gridTemplateColumns: 'repeat(8, max-content)',
+ gridTemplateColumns: 'repeat(10, max-content)',
gridGap: theme.spacing(0, 1),
position: 'absolute',
background: theme?.palette.greyAthens,
diff --git a/packages/ui-components/src/sections/Footer/styles.ts b/packages/ui-components/src/sections/Footer/styles.ts
index 38812cf70..cb4b27059 100644
--- a/packages/ui-components/src/sections/Footer/styles.ts
+++ b/packages/ui-components/src/sections/Footer/styles.ts
@@ -14,6 +14,8 @@ export const Inner = styled('div')<{ theme?: Theme }>(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
+ paddingLeft: 16,
+ paddingRight: 16,
width: '100%',
[`@media (min-width: ${theme?.breakPoints.medium}px)`]: {
minWidth: 400,
@@ -32,10 +34,12 @@ export const Left = styled('div')<{ theme?: Theme }>(({ theme }) => ({
[`@media (min-width: ${theme?.breakPoints.medium}px)`]: {
display: 'flex',
},
+ marginLeft: 1,
}));
export const Right = styled(Left)({
display: 'flex',
+ marginRight: 1,
});
export const Love = styled('span')<{ theme?: Theme }>(({ theme }) => ({
diff --git a/packages/ui-components/src/sections/Header/Header.test.tsx b/packages/ui-components/src/sections/Header/Header.test.tsx
index 5d7cdee08..03bae70c8 100644
--- a/packages/ui-components/src/sections/Header/Header.test.tsx
+++ b/packages/ui-components/src/sections/Header/Header.test.tsx
@@ -39,7 +39,7 @@ describe(' component with logged in state', () => {
cleanup();
});
- test('should load the component n logged out state', () => {
+ test('should load the component in logged out state', () => {
renderWithStore(
@@ -59,7 +59,9 @@ describe(' component with logged in state', () => {
,
store
);
- store.dispatch.login.logInUser({ username: 'store', token: '12345' });
+ act(() => {
+ store.dispatch.login.logInUser({ username: 'store', token: '12345' });
+ });
await waitFor(() => {
expect(screen.getByTestId('logInDialogIcon')).toBeTruthy();
@@ -78,13 +80,13 @@ describe(' component with logged in state', () => {
const loginBtn = screen.getByTestId('header--button-login');
fireEvent.click(loginBtn);
- const loginDialog = await waitFor(() => screen.getByTestId('login--dialog'));
+ const loginDialog = await waitFor(() => screen.findByTestId('login--dialog'));
expect(loginDialog).toBeTruthy();
});
test('should login and logout the user', async () => {
- const { getByText, getByTestId } = renderWithStore(
+ const { getByText, getByTestId, findByText, findByTestId } = renderWithStore(
,
@@ -104,14 +106,14 @@ describe(' component with logged in state', () => {
await act(async () => {
fireEvent.click(signInButton);
});
- await waitFor(() => getByTestId('logInDialogIcon'));
+ await waitFor(() => findByTestId('logInDialogIcon'));
const headerMenuAccountCircle = getByTestId('logInDialogIcon');
fireEvent.click(headerMenuAccountCircle);
// // wait for button Logout's appearance and return the element
- const logoutBtn = await waitFor(() => getByText('button.logout'));
+ const logoutBtn = await waitFor(() => findByText('button.logout'));
fireEvent.click(logoutBtn);
- await waitFor(() => getByText('button.login'));
+ await waitFor(() => findByText('button.login'));
expect(getByText('button.login')).toBeTruthy();
});
@@ -169,12 +171,12 @@ describe(' component with logged in state', () => {
expect(infoBtn).toBeInTheDocument();
fireEvent.click(infoBtn);
// wait for registrationInfo modal appearance and return the element
- const registrationInfoModal = await waitFor(() => screen.getByTestId('registryInfo--dialog'));
+ const registrationInfoModal = await waitFor(() => screen.findByTestId('registryInfo--dialog'));
expect(registrationInfoModal).toBeTruthy();
});
test('should close the registrationInfo modal when clicking on the button close', async () => {
- const { getByTestId, getByText, queryByTestId } = renderWithStore(
+ const { getByTestId, findByText, queryByTestId } = renderWithStore(
,
@@ -185,7 +187,7 @@ describe(' component with logged in state', () => {
fireEvent.click(infoBtn);
// wait for Close's button of registrationInfo modal appearance and return the element
- const closeBtn = await waitFor(() => getByText('button.close'));
+ const closeBtn = await waitFor(() => findByText('button.close'));
fireEvent.click(closeBtn);
const hasRegistrationInfoModalBeenRemoved = await waitForElementToBeRemoved(() =>
diff --git a/packages/ui-components/src/sections/Header/HeaderLeft.tsx b/packages/ui-components/src/sections/Header/HeaderLeft.tsx
index 5b5522cce..424834920 100644
--- a/packages/ui-components/src/sections/Header/HeaderLeft.tsx
+++ b/packages/ui-components/src/sections/Header/HeaderLeft.tsx
@@ -1,29 +1,37 @@
-import styled from '@emotion/styled';
+import Toolbar from '@mui/material/Toolbar';
import React from 'react';
-import { Link } from 'react-router-dom';
+import { Link as RouterLink } from 'react-router-dom';
import { Logo, Search } from '../../';
-import { LeftSide, SearchWrapper } from './styles';
+import { SearchWrapper } from './styles';
interface Props {
showSearch?: boolean;
}
-const StyledLink = styled(Link)({
- marginRight: '1em',
-});
-
const HeaderLeft: React.FC = ({ showSearch }) => (
-
-
+
+
-
+
{showSearch && (
)}
-
+
);
export default HeaderLeft;
diff --git a/packages/ui-components/src/sections/Header/HeaderMenu.tsx b/packages/ui-components/src/sections/Header/HeaderMenu.tsx
index 36544314a..1068e5821 100644
--- a/packages/ui-components/src/sections/Header/HeaderMenu.tsx
+++ b/packages/ui-components/src/sections/Header/HeaderMenu.tsx
@@ -39,7 +39,7 @@ const HeaderMenu: React.FC = ({
= ({
};
return (
-
+
{showSearch === true && (
= ({
)}
>
)}
-
+
);
};
diff --git a/packages/ui-components/src/sections/Header/HeaderSettingsDialog.tsx b/packages/ui-components/src/sections/Header/HeaderSettingsDialog.tsx
index f900f35ec..34d842263 100644
--- a/packages/ui-components/src/sections/Header/HeaderSettingsDialog.tsx
+++ b/packages/ui-components/src/sections/Header/HeaderSettingsDialog.tsx
@@ -9,9 +9,9 @@ import ReactMarkdown from 'react-markdown';
import { useSelector } from 'react-redux';
import { RootState, Theme } from '../../';
+import RegistryInfoDialog from '../../components/RegistryInfoDialog';
import LanguageSwitch from './LanguageSwitch';
import RegistryInfoContent from './RegistryInfoContent';
-import RegistryInfoDialog from './RegistryInfoDialog';
interface Props {
isOpen: boolean;
diff --git a/packages/ui-components/src/sections/Header/RegistryInfoContent/RegistryInfoContent.test.tsx b/packages/ui-components/src/sections/Header/RegistryInfoContent/RegistryInfoContent.test.tsx
index cbb5b8ef6..1a019233c 100644
--- a/packages/ui-components/src/sections/Header/RegistryInfoContent/RegistryInfoContent.test.tsx
+++ b/packages/ui-components/src/sections/Header/RegistryInfoContent/RegistryInfoContent.test.tsx
@@ -13,7 +13,7 @@ describe(' component', () => {
expect(screen.getByText('packageManagers.description')).toBeInTheDocument();
});
- test('should load the appropiate tab content when the tab is clicked', () => {
+ test('should load the appropiate tab content when the tab is clicked', () => {
const props = { registryUrl: 'http://localhost:4872', scope: '@' };
render( );
diff --git a/packages/ui-components/src/sections/Header/RegistryInfoDialog/RegistryInfoDialog.tsx b/packages/ui-components/src/sections/Header/RegistryInfoDialog/RegistryInfoDialog.tsx
deleted file mode 100644
index c20cb8836..000000000
--- a/packages/ui-components/src/sections/Header/RegistryInfoDialog/RegistryInfoDialog.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import Button from '@mui/material/Button';
-import Dialog from '@mui/material/Dialog';
-import DialogActions from '@mui/material/DialogActions';
-import React from 'react';
-import { useTranslation } from 'react-i18next';
-
-import { Content, Title } from './styles';
-import { Props } from './types';
-
-const RegistryInfoDialog: React.FC = ({ open = false, children, onClose, title = '' }) => {
- const { t } = useTranslation();
- return (
-
- {title}
- {children}
-
-
- {t('button.close')}
-
-
-
- );
-};
-
-export default RegistryInfoDialog;
diff --git a/packages/ui-components/src/sections/Header/RegistryInfoDialog/index.ts b/packages/ui-components/src/sections/Header/RegistryInfoDialog/index.ts
deleted file mode 100644
index 9c636b159..000000000
--- a/packages/ui-components/src/sections/Header/RegistryInfoDialog/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './RegistryInfoDialog';
diff --git a/packages/ui-components/src/sections/Header/RegistryInfoDialog/styles.ts b/packages/ui-components/src/sections/Header/RegistryInfoDialog/styles.ts
deleted file mode 100644
index a920eb7ce..000000000
--- a/packages/ui-components/src/sections/Header/RegistryInfoDialog/styles.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import styled from '@emotion/styled';
-import DialogContent from '@mui/material/DialogContent';
-import DialogTitle from '@mui/material/DialogTitle';
-
-import { Theme } from '../../../';
-
-export const Title = styled(DialogTitle)<{ theme?: Theme }>((props) => ({
- backgroundColor: props.theme?.palette.primary.main,
- color: props.theme?.palette.white,
- fontSize: props.theme?.fontSize.lg,
-}));
-
-export const Content = styled(DialogContent)<{ theme?: Theme }>(({ theme }) => ({
- padding: '0 24px',
- backgroundColor: theme?.palette.background.default,
-}));
-
-export const TextContent = styled('div')<{ theme?: Theme }>(({ theme }) => ({
- padding: '10px 24px',
- backgroundColor: theme?.palette.background.default,
-}));
diff --git a/packages/ui-components/src/sections/Header/RegistryInfoDialog/types.ts b/packages/ui-components/src/sections/Header/RegistryInfoDialog/types.ts
deleted file mode 100644
index 627b416b4..000000000
--- a/packages/ui-components/src/sections/Header/RegistryInfoDialog/types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { ReactNode } from 'react';
-
-export interface Props {
- children: ReactNode;
- open: boolean;
- title: string;
- onClose: () => void;
-}
diff --git a/packages/ui-components/src/sections/Header/styles.ts b/packages/ui-components/src/sections/Header/styles.ts
index 2726c1077..1fca2c8e4 100644
--- a/packages/ui-components/src/sections/Header/styles.ts
+++ b/packages/ui-components/src/sections/Header/styles.ts
@@ -4,27 +4,18 @@ import AppBar from '@mui/material/AppBar';
import IconButton from '@mui/material/IconButton';
import Toolbar from '@mui/material/Toolbar';
-import { Link, Theme } from '../../';
+import { Theme } from '../../Theme';
export const InnerNavBar = styled(Toolbar)({
justifyContent: 'space-between',
alignItems: 'center',
- padding: '0 15px',
+ padding: 0,
});
export const Greetings = styled('span')({
margin: '0 5px 0 0',
});
-export const RightSide = styled(Toolbar)({
- display: 'flex',
- padding: 0,
-});
-
-export const LeftSide = styled(RightSide)({
- flex: 1,
-});
-
export const MobileNavBar = styled('div')<{ theme?: Theme }>((props) => ({
alignItems: 'center',
display: 'flex',
@@ -50,6 +41,7 @@ export const SettingsButtom = styled(IconButton)({});
export const SearchWrapper = styled('div')({
display: 'none',
width: '100%',
+ marginLeft: 20,
});
export const NavBar = styled(AppBar)<{ theme?: Theme }>(({ theme }) => ({
@@ -83,7 +75,7 @@ export const NavBar = styled(AppBar)<{ theme?: Theme }>(({ theme }) => ({
`,
[`@media (min-width: ${theme?.breakPoints.large}px)`]: css`
${InnerNavBar} {
- padding: 0 20px;
+ padding: 0 16px;
}
`,
[`@media (min-width: ${theme?.breakPoints.xlarge}px)`]: css`
@@ -94,7 +86,3 @@ export const NavBar = styled(AppBar)<{ theme?: Theme }>(({ theme }) => ({
}
`,
}));
-
-export const StyledLink = styled(Link)<{ theme?: Theme }>(({ theme }) => ({
- color: theme?.palette.white,
-}));
diff --git a/packages/ui-components/src/sections/Home/Home.test.tsx b/packages/ui-components/src/sections/Home/Home.test.tsx
index 7c305772f..64de1706a 100644
--- a/packages/ui-components/src/sections/Home/Home.test.tsx
+++ b/packages/ui-components/src/sections/Home/Home.test.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { MemoryRouter } from 'react-router';
import { store } from '../../store';
-import { renderWithStore, screen, waitFor } from '../../test/test-react-testing-library';
+import { act, renderWithStore, screen, waitFor } from '../../test/test-react-testing-library';
import Home from './Home';
// force the windows to expand to display items
@@ -10,20 +10,28 @@ import Home from './Home';
jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600);
jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600);
-const ComponentSideBar: React.FC = () => (
+const ComponentHome: React.FC = () => (
);
describe('Home', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
test('should render titles', async () => {
- renderWithStore( , store);
+ act(() => {
+ renderWithStore( , store);
+ });
await waitFor(() => expect(screen.getAllByTestId('package-item-list')).toHaveLength(5));
});
test('should render loading', async () => {
- renderWithStore( , store);
+ act(() => {
+ renderWithStore( , store);
+ });
await waitFor(() => expect(screen.getByTestId('loading')).toBeInTheDocument());
});
});
diff --git a/packages/ui-components/src/sections/SideBar/Sidebar.test.tsx b/packages/ui-components/src/sections/SideBar/Sidebar.test.tsx
index 1df5280a0..bdca4478c 100644
--- a/packages/ui-components/src/sections/SideBar/Sidebar.test.tsx
+++ b/packages/ui-components/src/sections/SideBar/Sidebar.test.tsx
@@ -3,7 +3,7 @@ import { MemoryRouter } from 'react-router';
import { VersionProvider } from '../../providers';
import { store } from '../../store';
-import { renderWithStore, screen, waitFor } from '../../test/test-react-testing-library';
+import { act, renderWithStore, screen, waitFor } from '../../test/test-react-testing-library';
import Sidebar from './Sidebar';
jest.mock('marked');
@@ -31,37 +31,44 @@ describe('Sidebar', () => {
jest.clearAllMocks();
});
test('should render titles', async () => {
- renderWithStore( , store);
- await waitFor(() => expect(screen.getByText('jquery')).toBeInTheDocument());
+ act(() => {
+ renderWithStore( , store);
+ });
+ await waitFor(() => expect(screen.getAllByText('jquery')).toHaveLength(2));
- expect(screen.getByText(`jquery`)).toBeInTheDocument();
expect(screen.getByText(`sidebar.detail.latest-version`, { exact: false })).toBeInTheDocument();
expect(
- screen.getByText(`sidebar.detail.published a year ago`, { exact: false })
+ screen.getByText(/sidebar.detail.published .*years? ago/i, { exact: false })
).toBeInTheDocument();
expect(screen.getByText(`sidebar.installation.title`, { exact: false })).toBeInTheDocument();
});
test('should render commonJS', async () => {
- renderWithStore( , store);
-
- await waitFor(() => expect(screen.getByText('jquery')).toBeInTheDocument());
+ act(() => {
+ renderWithStore( , store);
+ });
+ // package name + keyword
+ await waitFor(() => expect(screen.getAllByText('jquery')).toHaveLength(2));
expect(screen.getByAltText('commonjs')).toBeInTheDocument();
});
test('should render typescript', async () => {
mockPkgName.mockReturnValue('glob');
- renderWithStore( , store);
-
+ act(() => {
+ renderWithStore( , store);
+ });
+ // just package name
await waitFor(() => expect(screen.getByText('glob')).toBeInTheDocument());
expect(screen.getByAltText('typescript')).toBeInTheDocument();
});
test('should render es modules', async () => {
mockPkgName.mockReturnValue('got');
- renderWithStore( , store);
-
- await waitFor(() => expect(screen.getByText('got')).toBeInTheDocument());
+ act(() => {
+ renderWithStore( , store);
+ });
+ // package name + keyword
+ await waitFor(() => expect(screen.getAllByText('got')).toHaveLength(2));
expect(screen.getByAltText('es6 modules')).toBeInTheDocument();
});
});
diff --git a/packages/ui-components/src/sections/SideBar/Sidebar.tsx b/packages/ui-components/src/sections/SideBar/Sidebar.tsx
index 5640a3701..51bac72eb 100644
--- a/packages/ui-components/src/sections/SideBar/Sidebar.tsx
+++ b/packages/ui-components/src/sections/SideBar/Sidebar.tsx
@@ -1,8 +1,6 @@
-import styled from '@emotion/styled';
import Paper from '@mui/material/Paper';
import React from 'react';
-import { Theme } from '../../Theme';
import ActionBar from '../../components/ActionBar';
import Author from '../../components/Author';
import Developers, { DeveloperType } from '../../components/Developers';
@@ -10,6 +8,7 @@ import Dist from '../../components/Distribution';
import Engines from '../../components/Engines';
import FundButton from '../../components/FundButton';
import Install from '../../components/Install';
+import Keywords from '../../components/Keywords';
import Repository from '../../components/Repository';
import SideBarTitle from '../../components/SideBarTitle';
import { useConfig } from '../../providers';
@@ -35,7 +34,7 @@ const DetailSidebar: React.FC = () => {
}
return (
-
+
{
showDownloadTarball={configOptions.showDownloadTarball}
showRaw={configOptions.showRaw}
/>
-
+
+
-
+
);
};
export default DetailSidebar;
-
-const StyledPaper = styled(Paper)<{ theme?: Theme }>(({ theme }) => ({
- padding: theme?.spacing(3, 2),
-}));
diff --git a/packages/ui-components/src/store/api.test.ts b/packages/ui-components/src/store/api.test.ts
index 03019e75f..1b93da41f 100644
--- a/packages/ui-components/src/store/api.test.ts
+++ b/packages/ui-components/src/store/api.test.ts
@@ -31,6 +31,22 @@ describe('api', () => {
expect(handled).toEqual([true, blob]);
});
+
+ test('should test pdf scenario', async () => {
+ const blob = new Blob(['foo']);
+ const blobPromise = Promise.resolve(blob);
+ const response: Response = {
+ url: 'http://localhost:8080/test.pdf',
+ blob: () => blobPromise,
+ ok: true,
+ headers: new Headers({
+ 'Content-Type': 'application/pdf',
+ }),
+ } as Response;
+ const handled = await handleResponseType(response);
+
+ expect(handled).toEqual([true, blob]);
+ });
});
describe('api client', () => {
diff --git a/packages/ui-components/src/store/models/configuration.ts b/packages/ui-components/src/store/models/configuration.ts
index d97330952..2810a8ead 100644
--- a/packages/ui-components/src/store/models/configuration.ts
+++ b/packages/ui-components/src/store/models/configuration.ts
@@ -7,6 +7,7 @@ import { Manifest, TemplateUIOptions } from '@verdaccio/types';
import type { RootModel } from '.';
import { colors } from '../../Theme';
import API from '../api';
+import { APIRoute } from './routes';
const defaultValues: TemplateUIOptions = {
primaryColor: colors.PRIMARY_COLOR,
@@ -39,7 +40,7 @@ export const configuration = createModel()({
},
effects: (dispatch) => ({
async getPackages() {
- const payload: Manifest[] = await API.request(`/-/verdaccio/packages`, 'GET');
+ const payload: Manifest[] = await API.request(APIRoute.CONFIG, 'GET');
dispatch.packages.savePackages(payload);
},
}),
diff --git a/packages/ui-components/src/store/models/login.ts b/packages/ui-components/src/store/models/login.ts
index fe1918efb..1bc0e580e 100644
--- a/packages/ui-components/src/store/models/login.ts
+++ b/packages/ui-components/src/store/models/login.ts
@@ -5,6 +5,8 @@ import type { RootModel } from '.';
import { isTokenExpire } from '../../utils';
import API from '../api';
import storage from '../storage';
+import { APIRoute } from './routes';
+import { stripTrailingSlash } from './utils';
export const HEADERS = {
JSON: 'application/json',
@@ -80,19 +82,15 @@ export const login = createModel()({
},
effects: (dispatch) => ({
async getUser({ username, password }, state) {
- const basePath = state.configuration.config.base;
+ const basePath = stripTrailingSlash(state.configuration.config.base);
try {
- const payload: LoginResponse = await API.request(
- `${basePath}-/verdaccio/sec/login`,
- 'POST',
- {
- body: JSON.stringify({ username, password }),
- headers: {
- Accept: HEADERS.JSON,
- 'Content-Type': HEADERS.JSON,
- },
- }
- );
+ const payload: LoginResponse = await API.request(`${basePath}${APIRoute.LOGIN}`, 'POST', {
+ body: JSON.stringify({ username, password }),
+ headers: {
+ Accept: HEADERS.JSON,
+ 'Content-Type': HEADERS.JSON,
+ },
+ });
dispatch.login.logInUser(payload);
dispatch.packages.getPackages(undefined);
} catch (error: any) {
diff --git a/packages/ui-components/src/store/models/manifest.ts b/packages/ui-components/src/store/models/manifest.ts
index 80f54094e..a31a523ad 100644
--- a/packages/ui-components/src/store/models/manifest.ts
+++ b/packages/ui-components/src/store/models/manifest.ts
@@ -5,6 +5,8 @@ import { Manifest } from '@verdaccio/types';
import type { RootModel } from '.';
import { PackageMetaInterface } from '../../types/packageMeta';
import API from '../api';
+import { APIRoute } from './routes';
+import { stripTrailingSlash } from './utils';
function isPackageVersionValid(
packageMeta: Partial,
@@ -83,10 +85,10 @@ export const manifest = createModel()({
},
effects: (dispatch) => ({
async getManifest({ packageName, packageVersion }, state) {
- const basePath = state.configuration.config.base;
+ const basePath = stripTrailingSlash(state.configuration.config.base);
try {
const manifest: Manifest = await API.request(
- `${basePath}-/verdaccio/data/sidebar/${packageName}${
+ `${basePath}${APIRoute.SIDEBAR}${packageName}${
packageVersion ? `?v=${packageVersion}` : ''
}`
);
@@ -98,10 +100,9 @@ export const manifest = createModel()({
}
const readme: string = await API.request(
- `${basePath}-/verdaccio/data/package/readme/${packageName}${
+ `${basePath}${APIRoute.README}${packageName}${
packageVersion ? `?v=${packageVersion}` : ''
- }`,
- 'GET'
+ }`
);
dispatch.manifest.saveManifest({ packageName, packageVersion, manifest, readme });
} catch (error: any) {
diff --git a/packages/ui-components/src/store/models/packages.ts b/packages/ui-components/src/store/models/packages.ts
index cef165365..44174757e 100644
--- a/packages/ui-components/src/store/models/packages.ts
+++ b/packages/ui-components/src/store/models/packages.ts
@@ -4,6 +4,8 @@ import { Manifest } from '@verdaccio/types';
import type { RootModel } from '.';
import API from '../api';
+import { APIRoute } from './routes';
+import { stripTrailingSlash } from './utils';
/**
*
@@ -23,12 +25,9 @@ export const packages = createModel()({
},
effects: (dispatch) => ({
async getPackages(_payload, state) {
- const basePath = state.configuration.config.base;
+ const basePath = stripTrailingSlash(state.configuration.config.base);
try {
- const payload: Manifest[] = await API.request(
- `${basePath}-/verdaccio/data/packages`,
- 'GET'
- );
+ const payload: Manifest[] = await API.request(`${basePath}${APIRoute.PACKAGES}`);
dispatch.packages.savePackages(payload);
} catch (error: any) {
// eslint-disable-next-line no-console
diff --git a/packages/ui-components/src/store/models/routes.ts b/packages/ui-components/src/store/models/routes.ts
new file mode 100644
index 000000000..63cedc99a
--- /dev/null
+++ b/packages/ui-components/src/store/models/routes.ts
@@ -0,0 +1,10 @@
+// Example API request:
+// http://localhost:8000/-/verdaccio/data/package/readme/jquery
+export enum APIRoute {
+ LOGIN = '/-/verdaccio/sec/login',
+ CONFIG = '/-/verdaccio/packages',
+ PACKAGES = '/-/verdaccio/data/packages',
+ SEARCH = '/-/verdaccio/data/search/', // :value
+ SIDEBAR = '/-/verdaccio/data/sidebar/', // :packageName?v=version
+ README = '/-/verdaccio/data/package/readme/', // :packageName?v=version
+}
diff --git a/packages/ui-components/src/store/models/search.ts b/packages/ui-components/src/store/models/search.ts
index 7f600973e..7756180d2 100644
--- a/packages/ui-components/src/store/models/search.ts
+++ b/packages/ui-components/src/store/models/search.ts
@@ -5,6 +5,8 @@ import { SearchResultWeb } from '@verdaccio/types';
import type { RootModel } from '.';
import API from '../api';
+import { APIRoute } from './routes';
+import { stripTrailingSlash } from './utils';
const CONSTANTS = {
API_DELAY: 300,
@@ -55,7 +57,7 @@ export const search = createModel()({
},
effects: (dispatch) => ({
async getSuggestions({ value }, state) {
- const basePath = state.configuration.config.base;
+ const basePath = stripTrailingSlash(state.configuration.config.base);
try {
const controller = new window.AbortController();
dispatch.search.addControllerToQueue({ controller });
@@ -63,7 +65,7 @@ export const search = createModel()({
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility
// FUTURE: signal is not well supported for IE and Samsung Browser
const suggestions: SearchResultWeb[] = await API.request(
- `${basePath}-/verdaccio/data/search/${encodeURIComponent(value)}`,
+ `${basePath}${APIRoute.SEARCH}${encodeURIComponent(value)}`,
'GET',
{
signal,
diff --git a/packages/ui-components/src/store/models/utils.ts b/packages/ui-components/src/store/models/utils.ts
new file mode 100644
index 000000000..fb9f6f815
--- /dev/null
+++ b/packages/ui-components/src/store/models/utils.ts
@@ -0,0 +1,3 @@
+export function stripTrailingSlash(url: string): string {
+ return url.replace(/\/$/, '');
+}
diff --git a/packages/ui-components/src/types/packageMeta.ts b/packages/ui-components/src/types/packageMeta.ts
index 576a64643..50839bf31 100644
--- a/packages/ui-components/src/types/packageMeta.ts
+++ b/packages/ui-components/src/types/packageMeta.ts
@@ -1,7 +1,7 @@
export type ModuleType = 'commonjs' | 'module';
export type Latest = {
- author?: Author;
+ author?: string | Author;
deprecated?: string;
name: string;
dist?: {
@@ -15,7 +15,7 @@ export type Latest = {
pnpm?: string;
yarn?: string;
};
- license?: undefined | LicenseInterface | string;
+ license?: string | LicenseInterface;
version: string;
homepage?: string;
bugs?: {
@@ -32,6 +32,7 @@ export type Latest = {
funding?: Funding;
maintainers?: Developer[];
contributors?: Developer[];
+ keywords?: string | string[];
};
export interface PackageMetaInterface {
@@ -44,8 +45,9 @@ export interface PackageMetaInterface {
export interface Developer {
name: string;
- email: string;
- avatar: string;
+ email?: string;
+ url?: string;
+ avatar?: string;
}
interface Funding {
@@ -75,7 +77,7 @@ export interface Version {
version: string;
author?: string | Author;
description?: string;
- license?: string;
+ license?: string | LicenseInterface;
main?: string;
keywords?: string[];
deprecated?: string;
diff --git a/packages/ui-components/src/utils/routes.ts b/packages/ui-components/src/utils/routes.ts
index 87f3472a9..184ca6805 100644
--- a/packages/ui-components/src/utils/routes.ts
+++ b/packages/ui-components/src/utils/routes.ts
@@ -1,5 +1,6 @@
export enum Route {
ROOT = '/',
+ DETAIL = '/-/web/detail/',
SCOPE_PACKAGE = '/-/web/detail/@:scope/:package',
SCOPE_PACKAGE_VERSION = '/-/web/detail/@:scope/:package/v/:version',
PACKAGE = '/-/web/detail/:package',
diff --git a/packages/ui-components/src/utils/utils.test.ts b/packages/ui-components/src/utils/utils.test.ts
index 29fe6bca0..89296e030 100644
--- a/packages/ui-components/src/utils/utils.test.ts
+++ b/packages/ui-components/src/utils/utils.test.ts
@@ -2,12 +2,14 @@ import MockDate from 'mockdate';
import { packageMeta } from './__partials__/packageMeta';
import {
+ fileSizeSI,
formatDate,
formatDateDistance,
formatLicense,
formatRepository,
getLastUpdatedPackageTime,
getRecentReleases,
+ getUplink,
} from './utils';
// jest.useFakeTimers().setSystemTime(new Date('2020-01-01'));
@@ -111,3 +113,35 @@ describe('getRecentReleases', (): void => {
expect(getRecentReleases()).toEqual([]);
});
});
+
+describe('getUplink', (): void => {
+ test('getUplink for npmjs', () => {
+ expect(getUplink('npmjs', 'semver')).toEqual('https://www.npmjs.com/package/semver');
+ });
+
+ test('getUplink for server1', () => {
+ expect(getUplink('server1', 'semver')).toEqual(null);
+ });
+});
+
+describe('fileSizeSI', (): void => {
+ test('fileSizeSI as number 1234567', () => {
+ expect(fileSizeSI(1234567)).toEqual('1.2 MB');
+ });
+
+ test('fileSizeSI as number 9876', () => {
+ expect(fileSizeSI(9876)).toEqual('9.9 kB');
+ });
+
+ test('fileSizeSI as number 1000', () => {
+ expect(fileSizeSI(1000)).toEqual('1.0 kB');
+ });
+
+ test('fileSizeSI as number 123', () => {
+ expect(fileSizeSI(123)).toEqual('123 Bytes');
+ });
+
+ test('fileSizeSI as number 0', () => {
+ expect(fileSizeSI(0)).toEqual('0 Bytes');
+ });
+});
diff --git a/packages/ui-components/src/utils/utils.ts b/packages/ui-components/src/utils/utils.ts
index 7304ca351..cf48401d8 100644
--- a/packages/ui-components/src/utils/utils.ts
+++ b/packages/ui-components/src/utils/utils.ts
@@ -105,6 +105,15 @@ export function getAuthorName(authorName?: string): string {
return authorName;
}
+export function getUplink(upLinkName: string, packageName: string): string | null {
+ // TODO: make this a config like "uplinks: npmjs: web: https://www.npmjs.com/package/"
+ switch (upLinkName) {
+ case 'npmjs':
+ return `https://www.npmjs.com/package/${packageName}`;
+ }
+ return null;
+}
+
export function fileSizeSI(
a: number,
b?: typeof Math,
@@ -112,9 +121,15 @@ export function fileSizeSI(
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')
- );
+ b = Math;
+ c = b.log;
+ d = 1e3;
+ e = (c(a) / c(d)) | 0;
+ let size = a / b.pow(d, e);
+ // no decimals for Bytes
+ if (e === 0) {
+ return Math.floor(size) + ' Bytes';
+ } else {
+ return size.toFixed(1) + ' ' + 'kMGTPEZY'[--e] + 'B';
+ }
}
diff --git a/packages/ui-components/tsconfig.json b/packages/ui-components/tsconfig.json
index a3ddf8580..758c177bf 100644
--- a/packages/ui-components/tsconfig.json
+++ b/packages/ui-components/tsconfig.json
@@ -1,5 +1,5 @@
{
- "extends": "../../../tsconfig.reference.json",
+ "extends": "../../tsconfig.reference.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",