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

\n

This 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
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
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 >
verdaccio user 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 >
verdaccio user 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 ( <> <StyledBox display="flex" flexWrap="wrap" margin="10px 0 10px 0"> - {visibleDevelopers.map((visibleDeveloper) => { + {visibleDevelopers.map((visibleDeveloper, index) => { return ( - <Tooltip key={visibleDeveloper.email} title={visibleDeveloper.name}> - <Avatar alt={visibleDeveloper.name} src={visibleDeveloper.avatar} /> - </Tooltip> + <Person + key={index} + packageName={packageName} + person={visibleDeveloper} + version={version} + /> ); })} {visibleDevelopersMax < developers.length && ( - <Fab onClick={handleSetVisibleDevelopersMax} size="small"> + <Fab data-testid={'fab-add'} onClick={handleSetVisibleDevelopersMax} size="small"> <Add /> </Fab> )} 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 <div class="emotion-2 MuiBox-root emotion-3" > - <div - aria-label="dmethvin" - class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4" + <a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4" data-mui-internal-clone-element="true" + data-testid="dmethvin" + href="mailto:test@gmail.com?subject=undefined v1.0.0" + rel="noopener noreferrer" + target="_blank" > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5" - data-testid="PersonIcon" - focusable="false" - viewBox="0 0 24 24" + <div + class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5" > - <path - d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" - /> - </svg> - </div> - <div - aria-label="mgol" - class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4" + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6" + data-testid="PersonIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + /> + </svg> + </div> + </a> + <a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4" data-mui-internal-clone-element="true" + data-testid="mgol" + href="mailto:m.goleb@gmail.com?subject=undefined v1.0.0" + rel="noopener noreferrer" + target="_blank" > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5" - data-testid="PersonIcon" - focusable="false" - viewBox="0 0 24 24" + <div + class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5" > - <path - d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" - /> - </svg> - </div> + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6" + data-testid="PersonIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + /> + </svg> + </div> + </a> </div> </div> </body>, @@ -150,10 +184,26 @@ exports[`test Developers should render the component for contributors with items } .emotion-3>* { - margin: 5px; + margin-right: 5px; } .emotion-4 { + margin: 0; + font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif; + font-weight: 400; + font-size: 0.75rem; + line-height: 1.66; + color: #4b5e40; + -webkit-text-decoration: none; + text-decoration: none; +} + +.emotion-4:hover { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.emotion-5 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -183,9 +233,13 @@ exports[`test Developers should render the component for contributors with items user-select: none; color: #f4f4f4; background-color: #bdbdbd; + width: 40px; + height: 40px; + margin-left: 0px; + margin-right: 8px; } -.emotion-5 { +.emotion-6 { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -213,40 +267,54 @@ exports[`test Developers should render the component for contributors with items <div class="emotion-2 MuiBox-root emotion-3" > - <div - aria-label="dmethvin" - class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4" + <a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4" data-mui-internal-clone-element="true" + data-testid="dmethvin" + href="mailto:test@gmail.com?subject=undefined v1.0.0" + rel="noopener noreferrer" + target="_blank" > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5" - data-testid="PersonIcon" - focusable="false" - viewBox="0 0 24 24" + <div + class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5" > - <path - d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" - /> - </svg> - </div> - <div - aria-label="mgol" - class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4" + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6" + data-testid="PersonIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + /> + </svg> + </div> + </a> + <a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4" data-mui-internal-clone-element="true" + data-testid="mgol" + href="mailto:m.goleb@gmail.com?subject=undefined v1.0.0" + rel="noopener noreferrer" + target="_blank" > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5" - data-testid="PersonIcon" - focusable="false" - viewBox="0 0 24 24" + <div + class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5" > - <path - d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" - /> - </svg> - </div> + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6" + data-testid="PersonIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + /> + </svg> + </div> + </a> </div> </div>, "debug": [Function], @@ -329,10 +397,26 @@ exports[`test Developers should render the component for maintainers with items } .emotion-3>* { - margin: 5px; + margin-right: 5px; } .emotion-4 { + margin: 0; + font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif; + font-weight: 400; + font-size: 0.75rem; + line-height: 1.66; + color: #4b5e40; + -webkit-text-decoration: none; + text-decoration: none; +} + +.emotion-4:hover { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.emotion-5 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -362,9 +446,13 @@ exports[`test Developers should render the component for maintainers with items user-select: none; color: #f4f4f4; background-color: #bdbdbd; + width: 40px; + height: 40px; + margin-left: 0px; + margin-right: 8px; } -.emotion-5 { +.emotion-6 { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -393,40 +481,54 @@ exports[`test Developers should render the component for maintainers with items <div class="emotion-2 MuiBox-root emotion-3" > - <div - aria-label="dmethvin" - class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4" + <a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4" data-mui-internal-clone-element="true" + data-testid="dmethvin" + href="mailto:test@gmail.com?subject=undefined v1.0.0" + rel="noopener noreferrer" + target="_blank" > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5" - data-testid="PersonIcon" - focusable="false" - viewBox="0 0 24 24" + <div + class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5" > - <path - d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" - /> - </svg> - </div> - <div - aria-label="mgol" - class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4" + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6" + data-testid="PersonIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + /> + </svg> + </div> + </a> + <a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4" data-mui-internal-clone-element="true" + data-testid="mgol" + href="mailto:m.goleb@gmail.com?subject=undefined v1.0.0" + rel="noopener noreferrer" + target="_blank" > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5" - data-testid="PersonIcon" - focusable="false" - viewBox="0 0 24 24" + <div + class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5" > - <path - d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" - /> - </svg> - </div> + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6" + data-testid="PersonIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + /> + </svg> + </div> + </a> </div> </div> </body>, @@ -453,10 +555,26 @@ exports[`test Developers should render the component for maintainers with items } .emotion-3>* { - margin: 5px; + margin-right: 5px; } .emotion-4 { + margin: 0; + font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif; + font-weight: 400; + font-size: 0.75rem; + line-height: 1.66; + color: #4b5e40; + -webkit-text-decoration: none; + text-decoration: none; +} + +.emotion-4:hover { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.emotion-5 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -486,9 +604,13 @@ exports[`test Developers should render the component for maintainers with items user-select: none; color: #f4f4f4; background-color: #bdbdbd; + width: 40px; + height: 40px; + margin-left: 0px; + margin-right: 8px; } -.emotion-5 { +.emotion-6 { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -516,40 +638,54 @@ exports[`test Developers should render the component for maintainers with items <div class="emotion-2 MuiBox-root emotion-3" > - <div - aria-label="dmethvin" - class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4" + <a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4" data-mui-internal-clone-element="true" + data-testid="dmethvin" + href="mailto:test@gmail.com?subject=undefined v1.0.0" + rel="noopener noreferrer" + target="_blank" > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5" - data-testid="PersonIcon" - focusable="false" - viewBox="0 0 24 24" + <div + class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5" > - <path - d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" - /> - </svg> - </div> - <div - aria-label="mgol" - class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-4" + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6" + data-testid="PersonIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + /> + </svg> + </div> + </a> + <a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-4" data-mui-internal-clone-element="true" + data-testid="mgol" + href="mailto:m.goleb@gmail.com?subject=undefined v1.0.0" + rel="noopener noreferrer" + target="_blank" > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-5" - data-testid="PersonIcon" - focusable="false" - viewBox="0 0 24 24" + <div + class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault emotion-5" > - <path - d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" - /> - </svg> - </div> + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiAvatar-fallback emotion-6" + data-testid="PersonIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" + /> + </svg> + </div> + </a> </div> </div>, "debug": [Function], 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('<Dist /> component', () => { expect(getByText('sidebar.distribution.size')).toBeInTheDocument(); expect(getByText('sidebar.distribution.size')).toBeInTheDocument(); expect(getByText('7', { exact: false })).toBeInTheDocument(); - expect(getByText('10.00 Bytes', { exact: false })).toBeInTheDocument(); + expect(getByText('10 Bytes', { exact: false })).toBeInTheDocument(); }); test('should render the component with license as string', () => { 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 }) => { <DistChip name={t('sidebar.distribution.size')}>{fileSizeSI(dist.unpackedSize)}</DistChip> ) : null} - <DistChip name={t('sidebar.distribution.license')}> - {formatLicense(license as string)} - </DistChip> + <DistChip name={t('sidebar.distribution.license')}>{formatLicense(license)}</DistChip> </DistListItem> </List> ); 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<EngineItemProps> = ({ title, element, engineText }) => ( <Grid item={true} xs={6}> <List subheader={<StyledText variant={'subtitle1'}>{title}</StyledText>}> <EngineListItem> - <Avatar sx={{ bgcolor: 'transparent' }}>{element}</Avatar> - <Typography variant="subtitle2">{engineText}</Typography> + <Avatar sx={{ backgroundColor: 'transparent', marginLeft: 0, padding: 0 }}> + {element} + </Avatar> + <Typography sx={{ margin: 0, padding: '0 0 0 10px' }} variant="subtitle2"> + {engineText} + </Typography> </EngineListItem> </List> </Grid> 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( + <ErrorBoundary> + <div>{'Test'}</div> + </ErrorBoundary> + ); + + expect(screen.getByText('Test')).toBeInTheDocument(); + }); + + test('should render error information when error is caught', () => { + const ErrorComponent = () => { + throw new Error('Test error'); + }; + + // Suppress error messages for this test + const spy = jest.spyOn(console, 'error'); + spy.mockImplementation(() => {}); + + render( + <ErrorBoundary> + <ErrorComponent /> + </ErrorBoundary> + ); + + expect(screen.getByText('Something went wrong.')).toBeInTheDocument(); + expect(screen.getByText(/error:/)).toBeInTheDocument(); + expect(screen.getByText(/info:/)).toBeInTheDocument(); + + // Restore console.error after test + spy.mockRestore(); + }); +}); 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 ( - <StyledLink external={true} to={fundingUrl} variant="button"> + <StyledLink to={fundingUrl} variant="button"> <Button color="primary" fullWidth={true} diff --git a/packages/ui-components/src/components/HeaderInfoDialog/HeaderInfoDialog.test.tsx b/packages/ui-components/src/components/HeaderInfoDialog/HeaderInfoDialog.test.tsx new file mode 100644 index 000000000..5f7394557 --- /dev/null +++ b/packages/ui-components/src/components/HeaderInfoDialog/HeaderInfoDialog.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import { fireEvent, render, screen } from '../../test/test-react-testing-library'; +import HeaderInfoDialog from './HeaderInfoDialog'; + +describe('HeaderInfoDialog', () => { + const onCloseDialog = jest.fn(); + + const tabs = [{ label: 'Tab 1' }, { label: 'Tab 2' }]; + + const tabPanels = [{ element: <div>{'Panel 1'}</div> }, { element: <div>{'Panel 2'}</div> }]; + + beforeEach(() => { + render( + <HeaderInfoDialog + dialogTitle="Dialog Title" + isOpen={true} + onCloseDialog={onCloseDialog} + tabPanels={tabPanels} + tabs={tabs} + /> + ); + }); + + test('renders without crashing', () => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + test('displays the dialog title', () => { + expect(screen.getByText('Dialog Title')).toBeInTheDocument(); + }); + + test('renders the tabs correctly', () => { + expect(screen.getByText('Tab 1')).toBeInTheDocument(); + expect(screen.getByText('Tab 2')).toBeInTheDocument(); + }); + + test('renders the tab panels correctly', async () => { + expect(screen.getByText('Panel 1')).toBeInTheDocument(); + // Panel 2 should not be visible initially + expect(screen.queryByText('Panel 2')).not.toBeInTheDocument(); + // Switch to Tab 2 + fireEvent.click(screen.getByText('Tab 2')); + await expect(screen.queryByText('Panel 2')).toBeInTheDocument(); + }); + + test('calls onCloseDialog when the dialog is closed', () => { + fireEvent.click(screen.getByRole('button')); + expect(onCloseDialog).toHaveBeenCalled(); + }); +}); 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<Props> = ({ <RegistryInfoDialog onClose={onCloseDialog} open={isOpen} title={dialogTitle}> <Box sx={{ width: '100%' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> - <Tabs aria-label="infoTabs" onChange={handleChange} value={value}> + <Tabs aria-label="infoTabs" data-testid={'tabs'} onChange={handleChange} value={value}> {tabs ? tabs.map((item, index) => { return ( @@ -68,7 +68,7 @@ const HeaderInfoDialog: React.FC<Props> = ({ {tabPanels ? tabPanels.map((item, index) => { return ( - <TabPanel index={index} key={item.key} value={value}> + <TabPanel index={index} key={index} value={value}> {item.element} </TabPanel> ); 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(<Heading>{'Test'}</Heading>); + const headingElement = screen.getByText('Test'); + expect(headingElement).toBeInTheDocument(); + expect(headingElement.tagName).toBe('H6'); + }); + + test('should render correctly with custom props', () => { + render(<Heading variant="h1">{'Test'}</Heading>); + const headingElement = screen.getByText('Test'); + expect(headingElement).toBeInTheDocument(); + expect(headingElement.tagName).toBe('H1'); + }); +}); 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 <ImgIcon alt="commonjs" height="20" src={icon} width="20" />; + return <img alt="commonjs" height="20" src={icon} width="20" />; } 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 <ImgIcon alt="es6 modules" height="20" src={icon} width="20" />; + return <img alt="es6 modules" height="20" src={icon} width="20" />; } 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 <ImgIcon alt="git" height="20" src={icon} width="20" />; + return <img alt="git" height="20" src={icon} width="20" />; } 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 <ImgIcon alt="nodejs" height="20" src={icon} width="20" />; + return <img alt="nodejs" height="20" src={icon} width="20" />; } 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 <ImgIcon alt="typescript" height="20" src={icon} width="20" />; + return <img alt="typescript" height="20" src={icon} width="20" />; } 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<SVGSVGElement> ) { return ( - <SvgIcon {...props} ref={ref}> + // eslint-disable-next-line verdaccio/jsx-spread + <SvgIcon viewBox="0 0 14 16" {...props} ref={ref}> <path d="M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM5 6.98L3.5 8.5 5 10l-.5 1L2 8.5 4.5 6l.5.98zM7.5 6L10 8.5 7.5 11l-.5-.98L8.5 8.5 7 7l.5-1z" /> </SvgIcon> ); 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 = () => ( <Box sx={{ width: '100%' }}> - <Stack spacing={2}> + <Stack direction="row" spacing={2}> + <Npm /> + <Pnpm /> + <Yarn /> + </Stack> + <Stack direction="row" spacing={2}> <NodeJS /> <Git /> - <Version /> + </Stack> + <Stack direction="row" spacing={2}> <TypeScript /> - <Time /> - <License /> - <Law /> <ES6Modules /> <CommonJS /> + </Stack> + <Stack direction="row" spacing={2}> + <Version /> + <Time /> + <FileBinary /> + <Law /> + </Stack> + <Stack direction="row" spacing={2}> + <License /> <Earth /> </Stack> </Box> 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( + <> + <Earth /> + <FileBinary /> + <Law /> + <License /> + <Time /> + <Version /> + </> + ); + expect(container.querySelectorAll('svg')).toHaveLength(6); + }); + + test('should render an IMG graphic linking to and SVG', () => { + const { container } = render( + <> + <CommonJS /> + <ES6Modules /> + <Git /> + <NodeJS /> + <TypeScript /> + <Npm /> + <Pnpm /> + <Yarn /> + </> + ); + expect(container.querySelectorAll('img')).toHaveLength(8); + }); + + test('should render small graphic', () => { + const { container } = render( + <SvgIcon size={'sm'}> + <circle cx="7" cy="7" r="7" /> + </SvgIcon> + ); + expect(container.querySelector('svg')).toHaveStyle('width: 14px'); + }); + + test('should render medium graphic', () => { + const { container } = render( + <SvgIcon size={'md'}> + <circle cx="7" cy="7" r="7" /> + </SvgIcon> + ); + expect(container.querySelector('svg')).toHaveStyle('width: 18px'); + }); +}); 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<typeof SvgIcon>; const Law = React.forwardRef(function Law(props: Props, ref: React.Ref<SVGSVGElement>) { return ( - <SvgIcon {...props} ref={ref}> + // eslint-disable-next-line verdaccio/jsx-spread + <SvgIcon viewBox="0 0 14 16" {...props} ref={ref}> <path d="M7 4c-.83 0-1.5-.67-1.5-1.5S6.17 1 7 1s1.5.67 1.5 1.5S7.83 4 7 4zm7 6c0 1.11-.89 2-2 2h-1c-1.11 0-2-.89-2-2l2-4h-1c-.55 0-1-.45-1-1H8v8c.42 0 1 .45 1 1h1c.42 0 1 .45 1 1H3c0-.55.58-1 1-1h1c0-.55.58-1 1-1h.03L6 5H5c0 .55-.45 1-1 1H3l2 4c0 1.11-.89 2-2 2H2c-1.11 0-2-.89-2-2l2-4H1V5h3c0-.55.45-1 1-1h4c.55 0 1 .45 1 1h3v1h-1l2 4zM2.5 7L1 10h3L2.5 7zM13 10l-1.5-3-1.5 3h3z" fillRule="evenodd" diff --git a/packages/ui-components/src/components/Icons/Managers/Npm.tsx b/packages/ui-components/src/components/Icons/Managers/Npm.tsx index 478a18591..fda9ede4a 100644 --- a/packages/ui-components/src/components/Icons/Managers/Npm.tsx +++ b/packages/ui-components/src/components/Icons/Managers/Npm.tsx @@ -1,13 +1,7 @@ -import styled from '@emotion/styled'; -import { Theme } from '@mui/material'; import React from 'react'; const icon = require('./npm.svg'); -const ImgIcon = styled.img<{ theme?: Theme }>(({ theme }) => ({ - marginLeft: theme?.spacing(1), -})); - export function Npm() { - return <ImgIcon alt="npm package manager" height="20" src={icon} width="20" />; + return <img alt="npm package manager" height="20" src={icon} width="20" />; } 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 <ImgIcon alt="pnpm package manager" height="20" src={icon} width="20" />; + return <img alt="pnpm package manager" height="20" src={icon} width="20" />; } 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 <ImgIcon alt="npm package manager" height="20" src={icon} width="20" />; + return <img alt="npm package manager" height="20" src={icon} width="20" />; } 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 <Install configOptions={configOptions} packageMeta={data} packageName="foo" />; + return <Install configOptions={configOptions} packageMeta={data} packageName={name} />; }; /* eslint-disable react/jsx-no-bind*/ @@ -22,6 +24,11 @@ describe('<Install />', () => { expect(screen.getByText('npm install foo@8.0.0')).toBeInTheDocument(); }); + test('should not render if name is missing', () => { + render(<ComponentToBeRendered name="" />); + expect(screen.queryByTestId('installList')).toBeNull(); + }); + test('should have 3 children', () => { window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['yarn', 'pnpm', 'npm']; const { getByTestId } = render(<ComponentToBeRendered />); @@ -32,9 +39,7 @@ describe('<Install />', () => { test('should have the element NPM', () => { window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['npm']; - render(<ComponentToBeRendered />); - expect(screen.getByText('sidebar.installation.title')).toBeTruthy(); expect(screen.queryByText('pnpm')).not.toBeInTheDocument(); expect(screen.queryByText('yarn')).not.toBeInTheDocument(); @@ -59,18 +64,54 @@ describe('<Install />', () => { }); }); -describe('getGlobalInstall', () => { - test('no global', () => { - expect(getGlobalInstall(false, 'foo', '1.0.0')).toEqual('1.0.0@foo'); - }); - test('global', () => { - expect(getGlobalInstall(true, 'foo', '1.0.0')).toEqual('-g 1.0.0@foo'); +describe('<InstallListItem />', () => { + test('renders correctly', () => { + render( + <InstallListItem + dependencyManager={DependencyManager.NPM} + packageName={'foo'} + packageVersion={'8.0.0'} + /> + ); + expect(screen.queryByTestId('installListItem-npm')).toBeInTheDocument(); }); - test('yarn no global', () => { - expect(getGlobalInstall(false, 'foo', '1.0.0', true)).toEqual('1.0.0@foo'); - }); - test('yarn global', () => { - expect(getGlobalInstall(true, 'foo', '1.0.0', true)).toEqual('1.0.0@foo'); + test('should not render if name is missing', () => { + render( + // @ts-ignore - testing invalid value + <InstallListItem dependencyManager={'other'} packageName={'foo'} packageVersion={'8.0.0'} /> + ); + // expect nothing to be rendered + expect(screen.queryByTestId('installListItem-npm')).toBeNull(); + }); +}); + +describe('getGlobalInstall', () => { + test('version', () => { + expect(getGlobalInstall(false, false, '1.0.0', 'foo')).toEqual('foo@1.0.0'); + }); + test('latest', () => { + expect(getGlobalInstall(true, false, '1.0.0', 'foo')).toEqual('foo'); + }); + + test('version global', () => { + expect(getGlobalInstall(false, true, '1.0.0', 'foo')).toEqual('-g foo@1.0.0'); + }); + test('latest global', () => { + expect(getGlobalInstall(true, true, '1.0.0', 'foo')).toEqual('-g foo'); + }); + + test('yarn version', () => { + expect(getGlobalInstall(false, false, '1.0.0', 'foo', true)).toEqual('foo@1.0.0'); + }); + test('yarn latest', () => { + expect(getGlobalInstall(true, false, '1.0.0', 'foo', true)).toEqual('foo'); + }); + + test('yarn version global', () => { + expect(getGlobalInstall(false, true, '1.0.0', 'foo', true)).toEqual('foo@1.0.0'); + }); + test('yarn latest global', () => { + expect(getGlobalInstall(true, true, '1.0.0', 'foo', true)).toEqual('foo'); }); }); 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<Props> = ({ packageMeta, packageName, configOptions }) => { const { t } = useTranslation(); - const theme = useTheme(); if (!packageMeta || !packageName) { return null; } @@ -38,16 +41,14 @@ const Install: React.FC<Props> = ({ packageMeta, packageName, configOptions }) = return hasPkgManagers ? ( <> - <Grid - container={true} - justifyContent="flex-end" - sx={{ marginRight: theme.spacing(10), alingText: 'right' }} - > - <SettingsMenu packageName={packageName} /> - </Grid> <List data-testid={'installList'} - subheader={<StyledText variant={'subtitle1'}>{t('sidebar.installation.title')}</StyledText>} + subheader={ + <Wrapper> + <StyledText variant={'subtitle1'}>{t('sidebar.installation.title')}</StyledText> + <SettingsMenu packageName={packageName} /> + </Wrapper> + } > {hasNpm && ( <InstallListItem diff --git a/packages/ui-components/src/components/Install/InstallListItem.tsx b/packages/ui-components/src/components/Install/InstallListItem.tsx index 11427d21b..fe374a710 100644 --- a/packages/ui-components/src/components/Install/InstallListItem.tsx +++ b/packages/ui-components/src/components/Install/InstallListItem.tsx @@ -22,9 +22,9 @@ const InstallListItemText = styled(ListItemText)({ }); const PackageMangerAvatar = styled(Avatar)({ - borderRadius: '0px', backgroundColor: 'transparent', padding: 0, + marginLeft: 0, }); export enum DependencyManager { @@ -39,10 +39,10 @@ interface Interface { packageVersion?: string; } -export function getGlobalInstall(isGlobal, packageVersion, packageName, isYarn = false) { +export function getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, isYarn = false) { const name = isGlobal - ? `${isYarn ? '' : '-g'} ${packageVersion ? `${packageName}@${packageVersion}` : packageName}` - : packageVersion + ? `${isYarn ? '' : '-g'} ${packageVersion && !isLatest ? `${packageName}@${packageVersion}` : packageName}` + : packageVersion && !isLatest ? `${packageName}@${packageVersion}` : packageName; @@ -56,6 +56,7 @@ const InstallListItem: React.FC<Interface> = ({ }) => { const { localSettings } = useSettings(); const theme = useTheme(); + const isLatest = localSettings[packageName]?.latest ?? false; const isGlobal = localSettings[packageName]?.global ?? false; switch (dependencyManager) { case DependencyManager.NPM: @@ -67,9 +68,9 @@ const InstallListItem: React.FC<Interface> = ({ <InstallListItemText primary={ <CopyToClipBoard - dataTestId="instalNpm" - text={`npm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`} - title={`npm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`} + dataTestId="installNpm" + text={`npm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`} + title={`npm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`} /> } /> @@ -93,7 +94,7 @@ const InstallListItem: React.FC<Interface> = ({ packageName, true )}` - : `yarn add ${getGlobalInstall(isGlobal, packageVersion, packageName, true)}` + : `yarn add ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, true)}` } title={ isGlobal @@ -103,7 +104,7 @@ const InstallListItem: React.FC<Interface> = ({ packageName, true )}` - : `yarn add ${getGlobalInstall(isGlobal, packageVersion, packageName, true)}` + : `yarn add ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName, true)}` } /> } @@ -120,8 +121,8 @@ const InstallListItem: React.FC<Interface> = ({ primary={ <CopyToClipBoard dataTestId="installPnpm" - text={`pnpm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`} - title={`pnpm install ${getGlobalInstall(isGlobal, packageVersion, packageName)}`} + text={`pnpm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`} + title={`pnpm install ${getGlobalInstall(isLatest, isGlobal, packageVersion, packageName)}`} /> } /> 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 ( + <ListItem sx={{ px: 0, mt: 0, flexWrap: 'wrap' }}> + {keywordList.sort().map((keyword, index) => ( + <Chip key={index} label={keyword} sx={{ mt: 1, mr: 1 }} /> + ))} + </ListItem> + ); +}; + +export default KeywordListItems; 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 = () => ( + <Keywords + packageMeta={{ + latest: { + name: 'verdaccio1', + version: '4.0.0', + keywords: ['verdaccio', 'npm', 'yarn'], + }, + }} + /> +); 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('<Keywords /> component', () => { + test('should render the component in default state', () => { + const packageMeta = { + latest: { + name: 'verdaccio1', + version: '4.0.0', + keywords: ['verdaccio', 'npm', 'yarn'], + }, + }; + + const container = render(<Keywords packageMeta={packageMeta} />); + + expect(container.getByText('sidebar.keywords.title')).toBeInTheDocument(); + expect(container.getByText('verdaccio')).toBeInTheDocument(); + expect(container.getByText('npm')).toBeInTheDocument(); + expect(container.getByText('yarn')).toBeInTheDocument(); + }); + + test('should not render if data is missing', () => { + // @ts-ignore + render(<Keywords packageMeta={{}} />); + expect(screen.queryByTestId('keyword-list')).toBeNull(); + + const packageMeta = { + latest: { + name: 'verdaccio1', + version: '4.0.0', + keywords: '', + }, + }; + + render(<Keywords packageMeta={packageMeta} />); + expect(screen.queryByTestId('keyword-list')).toBeNull(); + }); + + test('should render keywords set in string', () => { + const packageMeta = { + latest: { + name: 'verdaccio1', + version: '4.0.0', + keywords: 'hello, world, verdaccio', + }, + }; + + const container = render(<Keywords packageMeta={packageMeta} />); + + expect(container.getByText('sidebar.keywords.title')).toBeInTheDocument(); + expect(container.getByText('verdaccio')).toBeInTheDocument(); + expect(container.getByText('hello')).toBeInTheDocument(); + expect(container.getByText('world')).toBeInTheDocument(); + }); +}); 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 ( + <List + data-testid="keyword-list" + subheader={ + <Typography + sx={{ fontWeight: theme.fontWeight.bold, textTransform: 'capitalize' }} + variant="subtitle1" + > + {t('sidebar.keywords.title')} + </Typography> + } + > + <KeywordListItems keywords={packageMeta?.latest?.keywords} /> + </List> + ); +}; + +export default Keywords; 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('<Link /> component', () => { + test('should render the component in default state', () => { + const { container } = render( + <Router> + <Link to={'/'} /> + </Router> + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + test('should render the component with link', () => { + const { container } = render( + <Router> + <Link to={'/'}>{'Home'}</Link> + </Router> + ); + expect(container.firstChild).toMatchSnapshot(); + }); +}); 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<LinkRef, any>(function LinkFunction( - { external, to, children, variant, className, onClick }, +const Link = React.forwardRef<HTMLAnchorElement, any>(function LinkFunction( + { to, children, variant, className, onClick }, ref ) { - return external ? ( - <a - className={className} - href={to} - onClick={onClick} - ref={ref} - rel="noopener noreferrer" - target="_blank" - > - <Typography variant={variant ?? 'caption'}>{children}</Typography> - </a> - ) : ( + return ( <CustomRouterLink className={className} innerRef={ref} onClick={onClick} to={to}> <Typography variant={variant}>{children}</Typography> </CustomRouterLink> 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[`<Link /> component should render the component in default state 1`] = ` +.emotion-0 { + -webkit-text-decoration: none; + text-decoration: none; +} + +.emotion-0:hover, +.emotion-0:focus { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.emotion-2 { + margin: 0; + font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif; + font-weight: 400; + font-size: 1rem; + line-height: 1.5; +} + +<a + class="emotion-0 emotion-1" + href="/" +> + <p + class="MuiTypography-root MuiTypography-body1 emotion-2" + /> +</a> +`; + +exports[`<Link /> component should render the component with link 1`] = ` +.emotion-0 { + -webkit-text-decoration: none; + text-decoration: none; +} + +.emotion-0:hover, +.emotion-0:focus { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.emotion-2 { + margin: 0; + font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif; + font-weight: 400; + font-size: 1rem; + line-height: 1.5; +} + +<a + class="emotion-0 emotion-1" + href="/" +> + <p + class="MuiTypography-root MuiTypography-body1 emotion-2" + > + Home + </p> +</a> +`; 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('<LinkExternal /> component', () => { + test('should render the component in default state', () => { + const { container } = render(<LinkExternal to={'/'} />); + expect(container.firstChild).toMatchSnapshot(); + }); + + test('should render the component with external link', () => { + const { container } = render( + <LinkExternal to={'https://example.com'}>{'Example'}</LinkExternal> + ); + expect(container.firstChild).toMatchSnapshot(); + }); +}); 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<HTMLAnchorElement, any>((props, ref) => { + const { to, children, variant, ...rest } = props; + return ( + // eslint-disable-next-line verdaccio/jsx-spread + <Link + href={to} + ref={ref} + rel="noopener noreferrer" + target="_blank" + underline="hover" + variant={variant ?? 'caption'} + {...rest} + > + {children} + </Link> + ); +}); + +export default LinkExternal; 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[`<LinkExternal /> component should render the component in default state 1`] = ` +.emotion-0 { + margin: 0; + font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif; + font-weight: 400; + font-size: 0.75rem; + line-height: 1.66; + color: #4b5e40; + -webkit-text-decoration: none; + text-decoration: none; +} + +.emotion-0:hover { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +<a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-0" + href="/" + rel="noopener noreferrer" + target="_blank" +/> +`; + +exports[`<LinkExternal /> component should render the component with external link 1`] = ` +.emotion-0 { + margin: 0; + font-family: -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif; + font-weight: 400; + font-size: 0.75rem; + line-height: 1.66; + color: #4b5e40; + -webkit-text-decoration: none; + text-decoration: none; +} + +.emotion-0:hover { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +<a + class="MuiTypography-root MuiTypography-caption MuiLink-root MuiLink-underlineHover emotion-0" + href="https://example.com" + rel="noopener noreferrer" + target="_blank" +> + Example +</a> +`; 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('<Loading /> component', () => { - test('should render the component in default state', () => { - render(<Loading />); - screen.debug(); - expect(screen.getByTestId('loading')).toBeInTheDocument(); + test('should render the component in default state', async () => { + act(() => { + render(<Loading />); + }); + await waitFor(() => { + expect(screen.getByTestId('loading')).toBeInTheDocument(); + }); }); }); 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('<LoginDialog /> component', () => { fireEvent.change(userNameInput, { target: { value: 'xyz' } }); const passwordInput = screen.getByPlaceholderText('form-placeholder.password'); - expect(userNameInput).toBeInTheDocument(); + expect(passwordInput).toBeInTheDocument(); fireEvent.focus(passwordInput); await act(async () => { 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<Props> = ({ onClose, open = false }) => { const makeLogin = useCallback( async (username?: string, password?: string): Promise<LoginBody | void> => { // checks isEmpty - if (isEmpty(username) || isEmpty(password)) { + if (!username || !password || isEmpty(username) || isEmpty(password)) { dispatch.login.addError({ type: 'error', description: i18next.t('form-validation.username-or-password-cant-be-empty'), @@ -29,6 +29,15 @@ const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => { return; } + // checks min username and password length + if (username.length < 2 || password.length < 2) { + dispatch.login.addError({ + type: 'error', + description: i18next.t('form-validation.required-min-length', { length: 2 }), + }); + return; + } + try { dispatch.login.getUser({ username, password }); // const response: LoginBody = await doLogin(username as string, password as string); 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 ( <StyledSnackbarContent message={ - <Box alignItems="center" display="flex"> + <Box alignItems="center" data-testid="error" display="flex"> <StyledErrorIcon /> {error.description} </Box> 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 = () => ( + <Box sx={{ width: '100%' }}> + <Logo size="x-small" /> + <Logo size="small" /> + <Logo size="big" /> + </Box> +); 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('<Logo /> component', () => { test('should render the component in default state', () => { const { container } = render(<Logo />); expect(container.firstChild).toMatchSnapshot(); + expect(screen.getByTestId('default-logo')).toBeTruthy(); + }); + + test('should render custom logo', () => { + window.__VERDACCIO_BASENAME_UI_OPTIONS.logo = 'custom.png'; + render(<Logo />); + expect(screen.getByTestId('custom-logo')).toBeTruthy(); }); }); 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<Props> = ({ size, onClick, className, isDefault = false }) => { +const Logo: React.FC<Props> = ({ size, onClick, className, isDefault = false, title = '' }) => { const { configOptions } = useConfig(); + const theme = useTheme(); if (!isDefault && configOptions?.logo) { + const logoSrc = + theme?.palette.mode === 'dark' && configOptions.logoDark + ? configOptions.logoDark + : configOptions.logo; return ( <ImageLogo className={className} onClick={onClick}> - <img alt="logo" height="40px" src={configOptions.logo} /> + <img alt={title} data-testid={'custom-logo'} height="40px" src={logoSrc} /> </ImageLogo> ); } - return <StyledLogo className={className} onClick={onClick} size={size} />; + + return ( + <StyledLogo + className={className} + data-testid={'default-logo'} + onClick={onClick} + size={size} + title={title} + /> + ); }; export default Logo; 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[`<Logo /> component should render the component in default state 1`] = ` <div class="emotion-0 emotion-1" + data-testid="default-logo" + title="" /> `; 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('<Package /> component', () => { afterEach(() => { cleanup(); }); test('should load the component', () => { - const props = { - name: 'verdaccio', - version: '1.0.0', - time: String(dateOneMonthAgo()), - license: 'MIT', - description: 'Private NPM repository', - author: { - name: 'Sam', - }, - keywords: ['verdaccio'], - }; - const wrapper = renderWithStore( <MemoryRouter> <Package author={props.author} description={props.description} + dist={props.dist} + keywords={props.keywords} license={props.license} name={props.name} time={props.time} @@ -50,5 +63,25 @@ describe('<Package /> component', () => { expect(wrapper.getByText('MIT')).toBeInTheDocument(); }); - test.todo('should load the component without author'); + // test if click on download button will trigger the download action + test('should download the package', async () => { + renderWithStore( + <MemoryRouter> + <Package + author={props.author} + description={props.description} + dist={props.dist} + keywords={props.keywords} + license={props.license} + name={props.name} + time={props.time} + version={props.version} + /> + </MemoryRouter>, + store + ); + + fireEvent.click(screen.getByTestId('download-tarball')); + await waitFor(() => expect(store.getState().loading.models.download).toBe(true)); + }); }); 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<PackageInterface> = ({ description, dist, homepage, - keywords = [], + keywords, license, name: packageName, time, @@ -71,7 +70,6 @@ const Package: React.FC<PackageInterface> = ({ const config = useSelector((state: RootState) => state.configuration.config); const dispatch = useDispatch<Dispatch>(); const { t } = useTranslation(); - const theme = useTheme(); const isLoading = useSelector((state: RootState) => state?.loading?.models.download); const handleDownload = useCallback( @@ -94,21 +92,12 @@ const Package: React.FC<PackageInterface> = ({ const renderAuthorInfo = (): React.ReactNode => { const name = utils.getAuthorName(authorName); return ( - <Author> - <Avatar alt={name} src={authorAvatar} /> - <Details> - <div - style={{ - fontSize: '12px', - fontWeight: theme?.fontWeight.semiBold, - color: - theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white, - }} - > - {name} - </div> - </Details> - </Author> + <OverviewItem> + <Author> + <Avatar alt={name} src={authorAvatar} /> + <Details>{name}</Details> + </Author> + </OverviewItem> ); }; @@ -132,33 +121,34 @@ const Package: React.FC<PackageInterface> = ({ time && ( <OverviewItem> <StyledTime /> - <Published>{t('package.published-on', { time: utils.formatDate(time) })}</Published> - {utils.formatDateDistance(time)} + <Published title={utils.formatDate(time)}> + {t('package.published-on', { time: utils.formatDateDistance(time) })} + </Published> </OverviewItem> ); const renderHomePageLink = (): React.ReactNode => homepage && url.isURL(homepage) && ( - <Link external={true} to={homepage}> + <LinkExternal to={homepage}> <Tooltip aria-label={t('package.homepage')} title={t('package.visit-home-page')}> <IconButton aria-label={t('package.homepage')} size="large"> <HomeIcon /> </IconButton> </Tooltip> - </Link> + </LinkExternal> ); const renderBugsLink = (): React.ReactNode => bugs?.url && url.isURL(bugs.url) && ( - <Link external={true} to={bugs.url}> + <LinkExternal to={bugs.url}> <Tooltip aria-label={t('package.bugs')} title={t('package.open-an-issue')}> <IconButton aria-label={t('package.bugs')} size="large"> <BugReport /> </IconButton> </Tooltip> - </Link> + </LinkExternal> ); const renderDownloadLink = (): React.ReactNode => @@ -174,7 +164,11 @@ const Package: React.FC<PackageInterface> = ({ aria-label={t('package.download', { what: t('package.the-tar-file') })} title={t('package.tarball')} > - <IconButton aria-label={t('package.download')} size="large"> + <IconButton + aria-label={t('package.download')} + data-testid="download-tarball" + size="large" + > {isLoading ? ( <CircularProgress size={13}> <DownloadIcon /> @@ -191,7 +185,7 @@ const Package: React.FC<PackageInterface> = ({ return ( <Grid container={true} item={true} xs={12}> <Grid item={true} xs={11}> - <WrapperLink to={`/-/web/detail/${packageName}`}> + <WrapperLink to={`${Route.DETAIL}${packageName}`}> <PackageTitle className="package-title" data-testid="package-title"> {packageName} </PackageTitle> @@ -213,15 +207,7 @@ const Package: React.FC<PackageInterface> = ({ ); }; - const renderSecondaryComponent = (): React.ReactNode => { - const tags = keywords.sort().map((keyword, index) => <Tag key={index}>{keyword}</Tag>); - return ( - <> - <Description>{description}</Description> - {tags.length > 0 && <TagContainer>{tags}</TagContainer>} - </> - ); - }; + const renderSecondaryComponent = (): React.ReactNode => <Description>{description}</Description>; const renderPackageListItemText = (): React.ReactNode => ( <PackageListItemText @@ -231,10 +217,21 @@ const Package: React.FC<PackageInterface> = ({ /> ); + const renderKeywords = (): React.ReactNode => ( + <ListItem alignItems={'flex-start'} sx={{ py: 0, my: 0 }}> + <List sx={{ p: 0, my: 0 }}> + <KeywordListItems keywords={keywords} /> + </List> + </ListItem> + ); + return ( <Wrapper className={'package'} data-testid="package-item-list"> - <ListItem alignItems={'flex-start'}>{renderPackageListItemText()}</ListItem> - <ListItem alignItems={'flex-start'}> + <ListItem alignItems={'flex-start'} sx={{ mb: 0 }}> + {renderPackageListItemText()} + </ListItem> + {keywords && keywords?.length > 0 ? renderKeywords() : null} + <ListItem alignItems={'flex-start'} sx={{ mt: 0 }}> {renderAuthorInfo()} {renderVersionInfo()} {renderPublishedInfo()} @@ -248,8 +245,8 @@ const Package: React.FC<PackageInterface> = ({ export default Package; const iconStyle = ({ theme }: { theme: Theme }) => css` - margin: 2px 10px 0 0; - fill: ${theme?.palette.mode === 'light' ? theme?.palette.greyLight2 : theme?.palette.white}; + margin: 0 10px 0 0; + fill: ${theme?.palette.mode === 'light' ? theme?.palette.greyDark : theme?.palette.white}; `; const StyledVersion = styled(Version)` 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('<Tag /> component', () => { - test('should load the component in default state', () => { - const { container } = render( - <Tag> - <span>{'I am a child'}</span> - </Tag> - ); - expect(container.firstChild).toMatchSnapshot(); - }); -}); 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<Props> = ({ children }) => <Wrapper>{children}</Wrapper>; - -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[`<Tag /> component should load the component in default state 1`] = ` -.emotion-0 { - vertical-align: middle; - line-height: 22px; - border-radius: 2px; - color: #485a3e; - background-color: #f3f4f2; - padding: 0.22rem 0.4rem; - margin: 8px 8px 0 0; -} - -<span - class="emotion-0 emotion-1" -> - <span> - I am a child - </span> -</span> -`; 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('<PackageList /> 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 ( + <Person + packageName={mockPackageName} + person={mockPerson} + version={mockVersion} + withText={withText} + /> + ); +}; + +describe('Person component', () => { + test('should render avatar', () => { + render(<ComponentToBeRendered />); + const avatar = screen.getByAltText(mockPerson.name); + expect(avatar).toBeInTheDocument(); + // but not include text + expect(screen.queryByText(mockPerson.name)).not.toBeInTheDocument(); + }); + + test('should render name when withText is true', () => { + render(<ComponentToBeRendered withText={true} />); + const name = screen.getByText(mockPerson.name); + expect(name).toBeInTheDocument(); + }); + + test('should not render name when withText is false', async () => { + render(<ComponentToBeRendered />); + // hover over the avatar + fireEvent.mouseEnter(screen.getByTestId(mockPerson.name)); + // wait for the tooltip to appear + await screen.findByTestId(mockPerson.name + '-tooltip'); + }); +}); 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 = ( + <Avatar alt={person.name} src={person.avatar} sx={{ width: 40, height: 40, ml: 0, mr: 1 }} /> + ); + + return ( + <> + <Tooltip + data-testid={person.name} + key={person.email} + title={<PersonTooltip person={person} />} + > + {link.length > 0 ? ( + <LinkExternal to={link}>{avatarComponent}</LinkExternal> + ) : ( + avatarComponent + )} + </Tooltip> + {withText && ( + <Typography sx={{ ml: 1 }} variant="subtitle2"> + {getName(person.name)} + </Typography> + )} + </> + ); +}; + +export default Person; 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 }) => ( + <Typography data-testid={person.name + '-tooltip'}> + {person.name} + {person.email && url.isEmail(person.email) && ( + <LinkExternal to={`mailto:${person.email}`}> + <br /> + {person.email} + </LinkExternal> + )} + {person.url && url.isURL(person.url) && ( + <LinkExternal to={person.url}> + <br /> + {person.url} + </LinkExternal> + )} + </Typography> +); + +export default PersonTooltip; 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 ? ( <IconButton aria-label="close" + data-testid="close-raw-viewer" onClick={onClose} sx={{ position: 'absolute', @@ -46,18 +48,20 @@ type Props = { /* eslint-disable verdaccio/jsx-spread */ const RawViewer: React.FC<Props> = ({ isOpen = false, onClose, packageMeta }) => { const { t } = useTranslation(); + const theme = useTheme(); return ( <Dialog data-testid={'rawViewer--dialog'} fullScreen={true} open={isOpen}> <ViewerTitle id="viewer-title" onClose={onClose}> - {t('action-bar-action.raw')} + {t('action-bar-action.raw-title', { package: packageMeta.latest.name })} </ViewerTitle> <DialogContent> <ReactJson - collapseStringsAfterLength={40} - collapsed={true} - enableClipboard={false} + collapseStringsAfterLength={200} + collapsed={2} + enableClipboard={true} groupArraysAfterLength={10} src={packageMeta as any} + theme={theme?.palette.mode == 'light' ? 'bright:inverted' : 'bright'} /> </DialogContent> </Dialog> 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('<Readme /> component', () => { const wrapper = render(<Readme description="<h1>This is a test string</h1>" />); expect(wrapper.getByText('This is a test string')).toBeInTheDocument(); }); + + test('should sanitize html', () => { + const markdown = `<script>alert('test')</script>`; + const wrapper = render(<Readme description={markdown} />); + expect(wrapper.queryAllByText('test')).toHaveLength(0); + }); + + test('should highlight code', () => { + const markdown = `\`\`\`js\nconst test = 1 + 2;\n\`\`\``; + const wrapper = render(<Readme description={markdown} />); + 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<Props> = ({ 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 ( - <Card> + <Card sx={{ mb: 2 }}> <CardContent> - <Box data-testid="readme" sx={{ margin: theme.spacing(1) }}> + <Box data-testid="readme" sx={{ m: 2 }}> <Wrapper className={`markdown-body ${isDarkMode ? 'markdown-dark' : 'markdown-light'}`} dangerouslySetInnerHTML={{ __html: parseReadme(description) as string }} diff --git a/packages/ui-components/src/components/Readme/__snapshots__/Readme.spec.tsx.snap b/packages/ui-components/src/components/Readme/__snapshots__/Readme.spec.tsx.snap index f4e7f90b8..eb2391a09 100644 --- a/packages/ui-components/src/components/Readme/__snapshots__/Readme.spec.tsx.snap +++ b/packages/ui-components/src/components/Readme/__snapshots__/Readme.spec.tsx.snap @@ -12,6 +12,7 @@ exports[`<Readme /> 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[`<Readme /> component should load the component in default state 1`] = ` } .emotion-2 { - margin: 8px; + margin: 16px; } .emotion-3 ul { @@ -69,6 +70,7 @@ exports[`<Readme /> 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[`<Readme /> 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={<StyledText variant="subtitle1">{t('sidebar.repository.title')}</StyledText>} > <RepositoryListItem> - <RepositoryAvatar sx={{ backgroundColor: '#fff' }}> + <RepositoryAvatar sx={{ bgcolor: theme.palette.white }}> <Git /> </RepositoryAvatar> <RepositoryListItemText primary={ <CopyClipboard dataTestId="repositoryID" text={repositoryURL} title={repositoryURL}> - <GithubLink external={true} to={repositoryURL} variant="outline"> + <LinkExternal to={repositoryURL} variant="outline"> {repositoryURL} - </GithubLink> + </LinkExternal> </CopyClipboard> } /> 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('<Search /> component', () => { }); test('handleSearch: when user type package name in search component, show suggestions', async () => { - const { getByPlaceholderText, getAllByText } = renderWithStore( + const { getByPlaceholderText, findAllByText } = renderWithStore( <ComponentToBeRendered />, store ); @@ -57,14 +58,14 @@ describe('<Search /> 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( <ComponentToBeRendered />, store ); @@ -75,7 +76,7 @@ describe('<Search /> 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('<Search /> component', () => { }); test('handlePackagesClearRequested: should clear suggestions', async () => { - const { getByPlaceholderText, getAllByText } = renderWithStore( + const { getByPlaceholderText, findAllByText } = renderWithStore( <ComponentToBeRendered />, store ); @@ -120,18 +121,18 @@ describe('<Search /> 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( <ComponentToBeRendered />, store ); @@ -141,7 +142,7 @@ describe('<Search /> 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('<Search /> 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 = '<h1>verdaccio</h1>'; + 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 = '[![NPM version](https://img.shields.io/npm/latest.svg)]'; + 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<RouteComponentProps> = ({ history }) => { const { suggestions } = useSelector((state: RootState) => state.search); const isLoading = useSelector((state: RootState) => state?.loading?.models.search); const dispatch = useDispatch<Dispatch>(); + /** * Cancel all the requests which are in pending state. */ @@ -60,9 +62,9 @@ const Search: React.FC<RouteComponentProps> = ({ 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<SearchItemProps> = ({ // no action assigned by default }; return ( + // eslint-disable-next-line verdaccio/jsx-no-style <li {...props} style={{ flexDirection: 'column' }}> <Wrapper> <NameGroup> <Name>{name}</Name> - {description && <Description>{description}</Description>} + {description && <Description>{cleanDescription(description)}</Description>} </NameGroup> {version && <Version>{version}</Version>} </Wrapper> 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. <h1...>) + 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('<SettingsMenu />', () => { + test('should handle menu open and close', async () => { + render(<SettingsMenu packageName="foo" />); + const button = screen.getByRole('button'); + fireEvent.click(button); + await screen.findByRole('menu'); + // TODO onClose + }); + + test('should handle latest select', () => { + const { getByRole, getByText } = render(<SettingsMenu packageName="foo" />); + 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(<SettingsMenu packageName="foo" />); + 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(<SettingsMenu packageName="foo" />); + 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<Props> = ({ packageName }) => { +const SettingsMenu: React.FC<Props> = ({ packageName }) => { const { t } = useTranslation(); const { localSettings, updateSettings } = useSettings(); + const { configOptions } = useConfig(); const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const open = Boolean(anchorEl); const handleOpenMenu = (event: React.MouseEvent<HTMLButtonElement>) => { 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<Props> = ({ packageName }) => { setAnchorEl(null); }; + const statusLatest = localSettings[packageName]?.latest; const statusGlobal = localSettings[packageName]?.global; return ( <> @@ -56,10 +75,27 @@ const InstallListItem: React.FC<Props> = ({ packageName }) => { 'aria-labelledby': 'basic-button', }} anchorEl={anchorEl} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} id="basic-menu" onClose={handleClose} open={open} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} > + <MenuItem onClick={handleLatestSelect}> + {' '} + {statusLatest === true ? ( + <ListItemIcon> + <Check /> + </ListItemIcon> + ) : null} + {t('sidebar.installation.latest')} + </MenuItem> <MenuItem onClick={handleGlobalSelect}> {' '} {statusGlobal === true ? ( @@ -69,18 +105,20 @@ const InstallListItem: React.FC<Props> = ({ packageName }) => { ) : null} {t('sidebar.installation.global')} </MenuItem> - <MenuItem onClick={handleGlobalYarnModern}> - {' '} - {localSettings?.yarnModern ? ( - <ListItemIcon> - <Check /> - </ListItemIcon> - ) : null} - {t('sidebar.installation.yarnModern')} - </MenuItem> + {configOptions?.pkgManagers?.includes('yarn') && ( + <MenuItem onClick={handleGlobalYarnModern}> + {' '} + {localSettings?.yarnModern ? ( + <ListItemIcon> + <Check /> + </ListItemIcon> + ) : null} + {t('sidebar.installation.yarnModern')} + </MenuItem> + )} </Menu> </> ); }; -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 <CommonJS />; + return ( + <Icon> + <CommonJS /> + </Icon> + ); } else if (module === 'module') { - return <ES6Modules />; + return ( + <Icon> + <ES6Modules /> + </Icon> + ); } else { return null; } @@ -46,7 +58,11 @@ const DetailSidebarTitle: React.FC<Props> = ({ <TitleWrapper> <> {packageName} - {hasTypes && <TypeScript />} + {hasTypes && ( + <Icon> + <TypeScript /> + </Icon> + )} <ModuleJS module={moduleType} /> </> </TitleWrapper> 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('<UpLinks /> component', () => { expect(wrapper).toMatchSnapshot(); }); + + test('should not render if input is missing', () => { + const wrapper = render(<UpLinks packageMeta={undefined} />); + // 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 <NoItems data-testid="no-uplinks" text={t('uplinks.no-items', { name: latest.name })} />; + return ( + <Card sx={{ mb: 2 }}> + <CardContent> + <NoItems data-testid="no-uplinks" text={t('uplinks.no-items', { name: latest.name })} /> + </CardContent> + </Card> + ); } return ( - <> - <StyledText variant="subtitle1">{t('uplinks.title')}</StyledText> - <List> - {Object.keys(uplinks) - .reverse() - .map((name) => ( - <ListItem key={name}> - <ListItemText>{name}</ListItemText> - <Spacer /> - <ListItemText>{utils.formatDateDistance(uplinks[name].fetched)}</ListItemText> - </ListItem> - ))} - </List> - </> + <Card sx={{ mb: 2 }}> + <CardContent> + <Box data-testid="uplinks" sx={{ m: 2 }}> + <StyledText variant="subtitle1">{t('uplinks.title')}</StyledText> + <List dense={true}> + {Object.keys(uplinks) + .reverse() + .map((name) => ( + <ListItem + className="version-item" + data-testid={`uplink-${name}`} + key={name} + sx={{ pr: 0 }} + > + <ListItemText> + <UpLinkLink packageName={latest.name} uplinkName={name} /> + </ListItemText> + <Spacer /> + <ListItemText title={utils.formatDate(uplinks[name].fetched)}> + {utils.formatDateDistance(uplinks[name].fetched)} + </ListItemText> + </ListItem> + ))} + </List> + </Box> + </CardContent> + </Card> ); }; 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 ? ( + <LinkExternal to={link} variant="outline"> + {uplinkName} + </LinkExternal> + ) : ( + <>{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[`<UpLinks /> 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; -} - -<body> - <div> - <div - class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-standardInfo MuiAlert-standard emotion-0" - role="alert" - > - <div - class="MuiAlert-icon emotion-1" - > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeInherit emotion-2" - data-testid="InfoOutlinedIcon" - focusable="false" - viewBox="0 0 24 24" - > - <path - d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20, 12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10, 10 0 0,0 12,2M11,17H13V11H11V17Z" - /> - </svg> - </div> - <div - class="MuiAlert-message emotion-3" - > - <p - class="MuiTypography-root MuiTypography-body1 emotion-4" - data-testid="no-uplinks" - > - uplinks.no-items - </p> - </div> - </div> - </div> - </body>, - "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[`<UpLinks /> 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[`<UpLinks /> 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[`<UpLinks /> 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; +} + +<body> + <div> + <div + class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation1 MuiCard-root emotion-0" + > + <div + class="MuiCardContent-root emotion-1" + > + <div + class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-standardInfo MuiAlert-standard emotion-2" + role="alert" + > + <div + class="MuiAlert-icon emotion-3" + > + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeInherit emotion-4" + data-testid="InfoOutlinedIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20, 12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10, 10 0 0,0 12,2M11,17H13V11H11V17Z" + /> + </svg> + </div> + <div + class="MuiAlert-message emotion-5" + > + <p + class="MuiTypography-root MuiTypography-body1 emotion-6" + data-testid="no-uplinks" + > + uplinks.no-items + </p> + </div> + </div> + </div> + </div> + </div> + </body>, + "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[`<UpLinks /> component should render the component when there is no upli <div> <div - class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-standardInfo MuiAlert-standard emotion-0" - role="alert" + class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation1 MuiCard-root emotion-0" > <div - class="MuiAlert-icon emotion-1" + class="MuiCardContent-root emotion-1" > - <svg - aria-hidden="true" - class="MuiSvgIcon-root MuiSvgIcon-fontSizeInherit emotion-2" - data-testid="InfoOutlinedIcon" - focusable="false" - viewBox="0 0 24 24" + <div + class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-standardInfo MuiAlert-standard emotion-2" + role="alert" > - <path - d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20, 12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10, 10 0 0,0 12,2M11,17H13V11H11V17Z" - /> - </svg> - </div> - <div - class="MuiAlert-message emotion-3" - > - <p - class="MuiTypography-root MuiTypography-body1 emotion-4" - data-testid="no-uplinks" - > - uplinks.no-items - </p> + <div + class="MuiAlert-icon emotion-3" + > + <svg + aria-hidden="true" + class="MuiSvgIcon-root MuiSvgIcon-fontSizeInherit emotion-4" + data-testid="InfoOutlinedIcon" + focusable="false" + viewBox="0 0 24 24" + > + <path + d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20, 12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10, 10 0 0,0 12,2M11,17H13V11H11V17Z" + /> + </svg> + </div> + <div + class="MuiAlert-message emotion-5" + > + <p + class="MuiTypography-root MuiTypography-body1 emotion-6" + data-testid="no-uplinks" + > + uplinks.no-items + </p> + </div> + </div> </div> </div> </div>, @@ -260,7 +316,31 @@ exports[`<UpLinks /> component should render the component when there is no upli exports[`<UpLinks /> 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[`<UpLinks /> 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[`<UpLinks /> 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[`<UpLinks /> 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[`<UpLinks /> 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; } <body> <div> - <h6 - class="MuiTypography-root MuiTypography-subtitle1 emotion-0 emotion-1" + <div + class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation1 MuiCard-root emotion-0" > - uplinks.title - </h6> - <ul - class="MuiList-root MuiList-padding emotion-2" - > - <li - class="MuiListItem-root MuiListItem-gutters MuiListItem-padding emotion-3" + <div + class="MuiCardContent-root emotion-1" > <div - class="MuiListItemText-root emotion-4 emotion-5" + class="MuiBox-root emotion-2" + data-testid="uplinks" > - <span - class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary emotion-6" + <h6 + class="MuiTypography-root MuiTypography-subtitle1 emotion-3 emotion-4" > - npmjs - </span> - </div> - <div - class="emotion-7 emotion-8" - /> - <div - class="MuiListItemText-root emotion-4 emotion-5" - > - <span - class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary emotion-6" + uplinks.title + </h6> + <ul + class="MuiList-root MuiList-padding MuiList-dense emotion-5" > - 6 years ago - </span> + <li + class="MuiListItem-root MuiListItem-dense MuiListItem-gutters MuiListItem-padding version-item emotion-6" + data-testid="uplink-npmjs" + > + <div + class="MuiListItemText-root MuiListItemText-dense emotion-7 emotion-8" + > + <span + class="MuiTypography-root MuiTypography-body2 MuiListItemText-primary emotion-9" + > + <a + class="MuiTypography-root MuiTypography-outline MuiLink-root MuiLink-underlineHover emotion-10" + href="https://www.npmjs.com/package/verdaccio" + rel="noopener noreferrer" + target="_blank" + > + npmjs + </a> + </span> + </div> + <div + class="emotion-11 emotion-12" + /> + <div + class="MuiListItemText-root MuiListItemText-dense emotion-7 emotion-8" + title="06/23/2018 6:52:14 PM" + > + <span + class="MuiTypography-root MuiTypography-body2 MuiListItemText-primary emotion-9" + > + 6 years ago + </span> + </div> + </li> + </ul> </div> - </li> - </ul> + </div> + </div> </div> </body>, - "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[`<UpLinks /> 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[`<UpLinks /> 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[`<UpLinks /> 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[`<UpLinks /> 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; } <div> - <h6 - class="MuiTypography-root MuiTypography-subtitle1 emotion-0 emotion-1" + <div + class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation1 MuiCard-root emotion-0" > - uplinks.title - </h6> - <ul - class="MuiList-root MuiList-padding emotion-2" - > - <li - class="MuiListItem-root MuiListItem-gutters MuiListItem-padding emotion-3" + <div + class="MuiCardContent-root emotion-1" > <div - class="MuiListItemText-root emotion-4 emotion-5" + class="MuiBox-root emotion-2" + data-testid="uplinks" > - <span - class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary emotion-6" + <h6 + class="MuiTypography-root MuiTypography-subtitle1 emotion-3 emotion-4" > - npmjs - </span> - </div> - <div - class="emotion-7 emotion-8" - /> - <div - class="MuiListItemText-root emotion-4 emotion-5" - > - <span - class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary emotion-6" + uplinks.title + </h6> + <ul + class="MuiList-root MuiList-padding MuiList-dense emotion-5" > - 6 years ago - </span> + <li + class="MuiListItem-root MuiListItem-dense MuiListItem-gutters MuiListItem-padding version-item emotion-6" + data-testid="uplink-npmjs" + > + <div + class="MuiListItemText-root MuiListItemText-dense emotion-7 emotion-8" + > + <span + class="MuiTypography-root MuiTypography-body2 MuiListItemText-primary emotion-9" + > + <a + class="MuiTypography-root MuiTypography-outline MuiLink-root MuiLink-underlineHover emotion-10" + href="https://www.npmjs.com/package/verdaccio" + rel="noopener noreferrer" + target="_blank" + > + npmjs + </a> + </span> + </div> + <div + class="emotion-11 emotion-12" + /> + <div + class="MuiListItemText-root MuiListItemText-dense emotion-7 emotion-8" + title="06/23/2018 6:52:14 PM" + > + <span + class="MuiTypography-root MuiTypography-body2 MuiListItemText-primary emotion-9" + > + 6 years ago + </span> + </div> + </li> + </ul> </div> - </li> - </ul> + </div> + </div> </div>, "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<Props> = ({ versions, packageName, time }) = {Object.keys(listVersions) .reverse() .map((version) => ( - <ListItem className="version-item" data-testid={`version-${version}`} key={version}> - <Link to={`/-/web/detail/${packageName}/v/${version}`} variant="caption"> + <ListItem + className="version-item" + data-testid={`version-${version}`} + key={version} + sx={{ pr: 0 }} + > + <Link to={`${Route.DETAIL}${packageName}/v/${version}`} variant="outline"> <ListItemText disableTypography={false} primary={version}></ListItemText> </Link> {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<Props> = ({ tags, packageName, time }) => ( return time[tags[a]] < time[tags[b]] ? 1 : time[tags[a]] > time[tags[b]] ? -1 : 0; }) .map((tag) => ( - <ListItem className="version-item" data-testid={`tag-${tag}`} key={tag}> - <Link to={`/-/web/detail/${packageName}/v/${tags[tag]}`} variant="outline"> + <ListItem className="version-item" data-testid={`tag-${tag}`} key={tag} sx={{ pr: 0 }}> + <Link to={`${Route.DETAIL}${packageName}/v/${tags[tag]}`} variant="outline"> <ListItemText>{tag}</ListItemText> </Link> <Spacer /> 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('<Version /> 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<Props> = ({ packageMeta, packageName }) => { const { t } = useTranslation(); const { configOptions } = useConfig(); @@ -46,38 +56,53 @@ const Versions: React.FC<Props> = ({ packageMeta, packageName }) => { }; return ( - <> - {hasDistTags ? ( - <> - <Typography variant="subtitle1">{t('versions.current-tags')}</Typography> - <VersionsTagList packageName={packageName} tags={distTags} time={time} /> - </> - ) : null} - <> - <Typography variant="subtitle1">{t('versions.version-history')}</Typography> - <TextField - helperText={t('versions.search.placeholder')} - onChange={debounce((e) => { - filterVersions(e.target.value); - }, 200)} - size="small" - variant="standard" - /> - </> - {hasVersionHistory ? ( - <> - {hideDeprecatedVersions === true && ( - <Alert - severity="info" - sx={{ marginTop: theme.spacing(1), marginBottom: theme.spacing(1) }} - > - {t('versions.hide-deprecated')} - </Alert> - )} - <VersionsHistoryList packageName={packageName} time={time} versions={packageVersions} /> - </> - ) : null} - </> + <Card sx={{ mb: 2 }}> + <CardContent> + <Box data-testid="versions" sx={{ m: 2 }}> + {hasDistTags ? ( + <> + <StyledText variant="subtitle1"> + {t('versions.current-tags')} + <span>{` (${Object.keys(distTags).length})`}</span> + </StyledText> + <VersionsTagList packageName={packageName} tags={distTags} time={time} /> + </> + ) : null} + <> + <StyledText variant="subtitle1"> + {t('versions.version-history')} + <span>{` (${Object.keys(packageVersions).length})`}</span> + </StyledText> + <TextField + helperText={t('versions.search.placeholder')} + onChange={debounce((e) => { + filterVersions(e.target.value); + }, 200)} + size="small" + variant="standard" + width="50%" + /> + </> + {hasVersionHistory ? ( + <> + {hideDeprecatedVersions === true && ( + <Alert + severity="info" + sx={{ marginTop: theme.spacing(1), marginBottom: theme.spacing(1) }} + > + {t('versions.hide-deprecated')} + </Alert> + )} + <VersionsHistoryList + packageName={packageName} + time={time} + versions={packageVersions} + /> + </> + ) : null} + </Box> + </CardContent> + </Card> ); }; 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('<Header /> component with logged in state', () => { }); test('should load data from the provider', async () => { - await act(async () => { + act(() => renderWithStore( <MemoryRouter initialEntries={[`/-/web/detail/storybook`]}> <Route path={Routes.PACKAGE}> @@ -40,8 +40,8 @@ describe('<Header /> component with logged in state', () => { </Route> </MemoryRouter>, 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(<DetailContainer />); expect(container.firstChild).toMatchSnapshot(); }); + + test('renders without uplinks', () => { + window.__VERDACCIO_BASENAME_UI_OPTIONS.showUplinks = false; + render(<DetailContainer />); + 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 ( - <Box component="div" display="flex" flexDirection="column" padding={2}> - <Tabs onChange={handleChange} tabPosition={tabPosition} /> + <Box component="div" display="flex" flexDirection="column" padding={0}> + <Tabs + onChange={handleChange} + showUplinks={configOptions.showUplinks} + tabPosition={tabPosition} + /> {packageMeta?.latest?.deprecated && <Deprecated message={packageMeta?.latest?.deprecated} />} <ContainerContent readDescription={readMe} tabPosition={tabs[tabPosition]} /> </Box> 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<Props> = ({ tabPosition, onChange }) => { +const DetailContainerTabs: React.FC<Props> = ({ tabPosition, onChange, showUplinks }) => { const { t } = useTranslation(); return ( <Tabs onChange={onChange} value={tabPosition} variant={'fullWidth'}> <Tab data-testid={'readme-tab'} id={'readme-tab'} label={t('tab.readme')} /> <Tab data-testid={'dependencies-tab'} id={'dependencies-tab'} label={t('tab.dependencies')} /> <Tab data-testid={'versions-tab'} id={'versions-tab'} label={t('tab.versions')} /> - <Tab data-testid={'uplinks-tab'} id={'uplinks-tab'} label={t('tab.uplinks')} /> + {showUplinks && ( + <Tab data-testid={'uplinks-tab'} id={'uplinks-tab'} label={t('tab.uplinks')} /> + )} </Tabs> ); }; 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 = () => { <Icon> <FlagsIcon.TW /> </Icon> + <Icon> + <FlagsIcon.CA /> + </Icon> </Flags> </ToolTip> </Left> <Right> {configOptions?.version && ( <> - <span data-testid="version-footer">{t('footer.powered-by')}</span> - <Logo isDefault={true} onClick={goToVerdaccioWebsite} size="x-small" /> - {`/ ${configOptions.version}`} + <PoweredBy data-testid="version-footer">{t('footer.powered-by')}</PoweredBy> + <Logo + isDefault={true} + onClick={goToVerdaccioWebsite} + size="x-small" + title={configOptions.version} + /> </> )} </Right> @@ -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('<Header /> 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( <Router> <Header /> @@ -59,7 +59,9 @@ describe('<Header /> component with logged in state', () => { </Router>, 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('<Header /> 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( <Router> <Header /> </Router>, @@ -104,14 +106,14 @@ describe('<Header /> 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('<Header /> 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( <Router> <Header HeaderInfoDialog={CustomInfoDialog} /> </Router>, @@ -185,7 +187,7 @@ describe('<Header /> 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<Props> = ({ showSearch }) => ( - <LeftSide> - <StyledLink to={'/'}> + <Toolbar + sx={{ + display: 'flex', + padding: 0, + marginLeft: 1, + flex: 1, + '@media (min-width: 600px)': { + padding: 0, + marginLeft: 1, + marginRight: '20px', + }, + }} + > + <RouterLink to={'/'}> <Logo /> - </StyledLink> + </RouterLink> {showSearch && ( <SearchWrapper data-testid="search-container"> <Search /> </SearchWrapper> )} - </LeftSide> + </Toolbar> ); 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<Props> = ({ <Menu anchorEl={anchorEl} anchorOrigin={{ - vertical: 'top', + vertical: 'bottom', horizontal: 'right', }} onClose={onLoggedInMenuClose} diff --git a/packages/ui-components/src/sections/Header/HeaderRight.tsx b/packages/ui-components/src/sections/Header/HeaderRight.tsx index b39472978..805b0c1b4 100644 --- a/packages/ui-components/src/sections/Header/HeaderRight.tsx +++ b/packages/ui-components/src/sections/Header/HeaderRight.tsx @@ -1,11 +1,11 @@ import Button from '@mui/material/Button'; +import Toolbar from '@mui/material/Toolbar'; import React, { MouseEvent, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useCustomTheme } from '../../'; import HeaderMenu from './HeaderMenu'; import HeaderToolTip from './HeaderToolTip'; -import { RightSide } from './styles'; interface Props { showSearch?: boolean; @@ -75,7 +75,18 @@ const HeaderRight: React.FC<Props> = ({ }; return ( - <RightSide data-testid="header-right"> + <Toolbar + data-testid="header-right" + sx={{ + display: 'flex', + padding: 0, + marginRight: 0, + '@media (min-width: 600px)': { + padding: 0, + marginRight: 0, + }, + }} + > {showSearch === true && ( <HeaderToolTip onClick={onToggleMobileNav} @@ -124,7 +135,7 @@ const HeaderRight: React.FC<Props> = ({ )} </> )} - </RightSide> + </Toolbar> ); }; 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('<RegistryInfoContent /> 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(<RegistryInfoContent registryUrl={props.registryUrl} scope={props.scope} />); 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<Props> = ({ open = false, children, onClose, title = '' }) => { - const { t } = useTranslation(); - return ( - <Dialog - data-testid={'registryInfo--dialog'} - id="registryInfo--dialog-container" - maxWidth="sm" - onClose={onClose} - open={open} - > - <Title>{title} - {children} - - - - - ); -}; - -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",