Compare commits

..

7 Commits

Author SHA1 Message Date
Juan Picado @jotadeveloper
45f76bfc2c Merge branch 'master' into juanpicado-patch-1 2019-11-23 13:48:11 +01:00
Juan Picado @jotadeveloper
b03e6d6c27 Merge branch 'master' into juanpicado-patch-1 2019-09-13 23:52:33 -07:00
Juan Picado @jotadeveloper
cdfbde1df1 Merge branch 'master' into juanpicado-patch-1 2019-08-12 07:46:00 +02:00
Juan Picado @jotadeveloper
521e11a453 Merge branch 'master' into juanpicado-patch-1 2019-08-04 22:15:22 +02:00
Juan Picado @jotadeveloper
fd4d7037ed Merge branch '4.x-master' into juanpicado-patch-1 2019-06-26 09:58:51 +02:00
Juan Picado @jotadeveloper
c918518f69 Merge branch '4.x-master' into juanpicado-patch-1 2019-06-03 21:21:23 +02:00
Juan Picado @jotadeveloper
5ff4b8a184 feat: add CONTRIBUTING.md 2019-05-26 18:53:40 +02:00
182 changed files with 4694 additions and 15105 deletions

View File

@@ -1,8 +1,4 @@
{
"presets": [["@verdaccio"]],
"plugins": [
"emotion",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator"
]
"plugins": ["emotion"]
}

View File

@@ -136,7 +136,7 @@ jobs:
command: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
- run:
name: Publish
command: npm publish
command: yarn publish
workflows:
version: 2

View File

@@ -45,8 +45,11 @@
}
}
],
"@typescript-eslint/explicit-function-return-type": 0,
"react/display-name": 0,
"@typescript-eslint/explicit-function-return-type": ["warn",
{
"allowExpressions": true,
"allowTypedFunctionExpressions": true
}],
"react/no-deprecated": 1,
"react/jsx-no-target-blank": 1,
"react/destructuring-assignment": ["error", "always"],
@@ -72,7 +75,7 @@
"arrow": "parens",
"condition": "parens",
"logical": "parens",
"prop": "ignore"
"prop": "parens"
}],
"react/jsx-boolean-value": ["error", "always"],
"react/jsx-closing-tag-location": ["error"],
@@ -83,7 +86,7 @@
"react/jsx-indent": ["error", 2],
"react/jsx-indent-props": ["error", 2],
"react/jsx-key": ["error"],
"react/jsx-max-depth":["error", { "max": 5}],
"react/jsx-max-depth": ["error", { "max": 2}],
"react/jsx-max-props-per-line": ["error", {"maximum": 3, "when": "multiline" }],
"react/jsx-no-bind": ["error"],
"react/jsx-no-comment-textnodes": ["error"],

View File

@@ -1,11 +0,0 @@
name: Security Flow
on: push
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@0.1.0
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

View File

@@ -37,4 +37,4 @@
],
"results": {},
"version": "0.12.4"
}
}

View File

View File

@@ -7,4 +7,4 @@
"typescriptreact"
],
"typescript.tsdk": "node_modules/typescript/lib"
}
}

View File

@@ -1,2 +1 @@
save-prefix ""
registry "https://registry.verdaccio.org"

View File

@@ -2,125 +2,6 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.3.0](https://github.com/verdaccio/ui/compare/v1.0.4...v1.3.0) (2020-04-01)
### Features
* **style:** added dark mode ([#446](https://github.com/verdaccio/ui/issues/446)) ([cdad5cf](https://github.com/verdaccio/ui/commit/cdad5cf70d69b7bb045fce461a32108def81721d))
### Bug Fixes
* **chore:** droped release ([#449](https://github.com/verdaccio/ui/issues/449)) ([f321f7b](https://github.com/verdaccio/ui/commit/f321f7b6fe1ac44897753f0bfdbbaa6ca7eca515))
## [1.2.0](https://github.com/verdaccio/ui/compare/v1.0.4...v1.2.0) (2020-04-01)
### Features
* **style:** added dark mode ([#446](https://github.com/verdaccio/ui/issues/446)) ([cdad5cf](https://github.com/verdaccio/ui/commit/cdad5cf70d69b7bb045fce461a32108def81721d))
### Bug Fixes
* **chore:** droped release ([#449](https://github.com/verdaccio/ui/issues/449)) ([f321f7b](https://github.com/verdaccio/ui/commit/f321f7b6fe1ac44897753f0bfdbbaa6ca7eca515))
## [1.1.0](https://github.com/verdaccio/ui/compare/v1.0.4...v1.1.0) (2020-04-01)
### Features
* **style:** added dark mode ([#446](https://github.com/verdaccio/ui/issues/446)) ([cdad5cf](https://github.com/verdaccio/ui/commit/cdad5cf70d69b7bb045fce461a32108def81721d))
### Bug Fixes
* **chore:** droped release ([#449](https://github.com/verdaccio/ui/issues/449)) ([f321f7b](https://github.com/verdaccio/ui/commit/f321f7b6fe1ac44897753f0bfdbbaa6ca7eca515))
### [1.0.4](https://github.com/verdaccio/ui/compare/v1.0.0...v1.0.4) (2020-03-17)
## [1.0.0](https://github.com/verdaccio/ui/compare/v0.3.13...v1.0.0) (2020-03-13)
### Features
* **de-translations:** added de-DE translations to the UI ([#441](https://github.com/verdaccio/ui/issues/441)) ([e27d59b](https://github.com/verdaccio/ui/commit/e27d59bff7039473e566090fa0f825f7e462aa4e))
* spanish translations to UI ([#440](https://github.com/verdaccio/ui/issues/440)) ([0abe1ef](https://github.com/verdaccio/ui/commit/0abe1ef41ca93b900ddda72e2d873ee52078221c))
* **i18n:** added i18next for user interface translations ([#432](https://github.com/verdaccio/ui/issues/432)) ([7428384](https://github.com/verdaccio/ui/commit/7428384b55e6089dbe45e6b216eee0b670dff576))
### [0.3.13](https://github.com/verdaccio/ui/compare/v0.3.12...v0.3.13) (2020-02-02)
### Bug Fixes
* do not capitalize heading - closes [#428](https://github.com/verdaccio/ui/issues/428) ([#431](https://github.com/verdaccio/ui/issues/431)) ([d481f54](https://github.com/verdaccio/ui/commit/d481f549484361c1d1bc011e0858e8f99b8a2528))
* package list refresh based on logged-in user ([#415](https://github.com/verdaccio/ui/issues/415)) ([222ffed](https://github.com/verdaccio/ui/commit/222ffed0226f5aaa62f2d5b91bb08717b2aa24ef)), closes [#414](https://github.com/verdaccio/ui/issues/414) [#414](https://github.com/verdaccio/ui/issues/414)
* reload packages on log in ([#421](https://github.com/verdaccio/ui/issues/421)) ([1eca1f4](https://github.com/verdaccio/ui/commit/1eca1f40797790e87d9592204ca061527d09c4ae))
* typo ([#423](https://github.com/verdaccio/ui/issues/423)) ([164cea6](https://github.com/verdaccio/ui/commit/164cea6c10804c1d2097c2a582eb3e1e51814d4a))
* update dependencies ([#420](https://github.com/verdaccio/ui/issues/420)) ([ee1c3f0](https://github.com/verdaccio/ui/commit/ee1c3f08eb16da2313d8841cfab18358d7f4ea10))
### [0.3.12](https://github.com/verdaccio/ui/compare/v0.3.11...v0.3.12) (2020-01-09)
### Bug Fixes
* generate correct registry URL ([#413](https://github.com/verdaccio/ui/issues/413)) ([6b322ad](https://github.com/verdaccio/ui/commit/6b322ad553e9fb3ee65b2968dcfe856ba42a0bfb)), closes [#300](https://github.com/verdaccio/ui/issues/300) [#311](https://github.com/verdaccio/ui/issues/311)
### [0.3.11](https://github.com/verdaccio/ui/compare/v0.3.10...v0.3.11) (2020-01-08)
### Bug Fixes
* remove prevent default and use react context ([#411](https://github.com/verdaccio/ui/issues/411)) ([6bd38b8](https://github.com/verdaccio/ui/commit/6bd38b812032857bb19af8978d48f6f8969af6cf))
* removed unused style file ([#406](https://github.com/verdaccio/ui/issues/406)) ([6eeae63](https://github.com/verdaccio/ui/commit/6eeae630ef441a871d06b888b6a21178e36e0db7))
### [0.3.10](https://github.com/verdaccio/ui/compare/v0.3.9...v0.3.10) (2019-12-30)
### Features
* added "Fund this package" button ([#375](https://github.com/verdaccio/ui/issues/375)) ([bf093cc](https://github.com/verdaccio/ui/commit/bf093cc27b8625cdc50dbfc9b8dd7e37f4e24da9))
### Bug Fixes
* add missing trailing slash to publicPath - closes [#395](https://github.com/verdaccio/ui/issues/395) ([#396](https://github.com/verdaccio/ui/issues/396)) ([bae9638](https://github.com/verdaccio/ui/commit/bae9638b23b70eff78b78b8ca52ff40162333354))
* engine warning on console for ui ([#403](https://github.com/verdaccio/ui/issues/403)) ([d554049](https://github.com/verdaccio/ui/commit/d554049699494e946f4caf345177839b4f0cba8b))
* remove background from styled Avatar components - closes [#371](https://github.com/verdaccio/ui/issues/371) ([#398](https://github.com/verdaccio/ui/issues/398)) ([787dda4](https://github.com/verdaccio/ui/commit/787dda4a016a1fcd1142bd4b705e2c71e232d13e))
* remove double padding and add missing background color - closes [#373](https://github.com/verdaccio/ui/issues/373) ([#399](https://github.com/verdaccio/ui/issues/399)) ([797c238](https://github.com/verdaccio/ui/commit/797c2381e453d4f40e1703402f192eb7675d6fbe))
* remove whitespace from logo image - closes [#374](https://github.com/verdaccio/ui/issues/374) ([#400](https://github.com/verdaccio/ui/issues/400)) ([544b999](https://github.com/verdaccio/ui/commit/544b999f81e39557e0fc002d21b24c512cfebc54))
### [0.3.9](https://github.com/verdaccio/ui/compare/v0.3.8...v0.3.9) (2019-12-14)
### [0.3.8](https://github.com/verdaccio/ui/compare/v0.3.7...v0.3.8) (2019-12-14)
### Features
* login Dialog Component - Replaced class by func. comp + added react-hook-form ([#341](https://github.com/verdaccio/ui/issues/341)) ([42d3bb8](https://github.com/verdaccio/ui/commit/42d3bb8508c666c28250432ada734d58ccb0eca8))
### Bug Fixes
* formatDate ([#308](https://github.com/verdaccio/ui/issues/308)) ([33f873a](https://github.com/verdaccio/ui/commit/33f873a8c78e419a36e3a29f7ea216714172b174))
* removed deade import ([#346](https://github.com/verdaccio/ui/issues/346)) ([ae617a5](https://github.com/verdaccio/ui/commit/ae617a5c04ad1b82309d36d3bdcf6b6b6fd925d0))
* updated actionbar snap ([#340](https://github.com/verdaccio/ui/issues/340)) ([09b831a](https://github.com/verdaccio/ui/commit/09b831a40d4e82a122f8fae3e45bdd161a3281bb))
### [0.3.7](https://github.com/verdaccio/ui/compare/v0.3.6...v0.3.7) (2019-11-24)
### Features
* Added Theme and migrate to emotion@10.x 🚀 ([#286](https://github.com/verdaccio/ui/issues/286)) ([111f0c5](https://github.com/verdaccio/ui/commit/111f0c50e5053202ca55fe4f3f28dd30e4932240))
### Bug Fixes
* **#300:** correctly reference registry url from options ([ee74474](https://github.com/verdaccio/ui/commit/ee74474811eb609072e1678bcb90db33756dcf38)), closes [#300](https://github.com/verdaccio/ui/issues/300)
* restore lint-staged@8.2.1 ([dbaa0c4](https://github.com/verdaccio/ui/commit/dbaa0c43b8104b350e4907387f89d4e9e719741f))
* update snapshots ([fd306de](https://github.com/verdaccio/ui/commit/fd306def9535d9168dc79ab020ec288a4d5df1a8))
### [0.3.6](https://github.com/verdaccio/ui/compare/v0.3.5...v0.3.6) (2019-11-08)
### [0.3.5](https://github.com/verdaccio/ui/compare/v0.3.4...v0.3.5) (2019-11-07)
@@ -405,4 +286,4 @@ All notable changes to this project will be documented in this file. See [standa
<a name="0.0.3"></a>
## 0.0.3 (2019-04-04)
## 0.0.3 (2019-04-04)

3
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,3 @@
# Contributing Guidelines
// TODO

View File

@@ -12,7 +12,7 @@
[![stackshare](https://img.shields.io/badge/Follow%20on-StackShare-blue.svg?logo=stackshare&style=flat)](https://stackshare.io/verdaccio)
[![discord](https://img.shields.io/discord/388674437219745793.svg)](http://chat.verdaccio.org/)
[![node](https://img.shields.io/node/v/@verdaccio/ui-theme/latest.svg)](https://www.npmjs.com/package/@verdaccio/ui-theme)
[![MIT](https://img.shields.io/github/license/mashape/apistatus.svg)](./LICENSE)
![MIT](https://img.shields.io/github/license/mashape/apistatus.svg)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/verdaccio/localized.svg)](https://crowdin.com/project/verdaccio)
[![codecov](https://codecov.io/gh/verdaccio/ui/branch/master/graph/badge.svg)](https://codecov.io/gh/verdaccio/ui)
@@ -22,7 +22,7 @@
## Contributing
We use `>=yarn@1.13.0`, keep in mind that we use lockfiles and use at least Node `v10.13.0` to be able to build the project.
We use `>=yarn@1.13.0`, keep on mind we use lock file.
For development run the following command, it will execute `webpack` and `verdaccio` to
@@ -93,7 +93,7 @@ If you have any issue you can try the following options, do no desist to ask or
* [Blog](https://medium.com/verdaccio)
* [Donations](https://opencollective.com/verdaccio)
* [Roadmaps](https://github.com/verdaccio/ui/projects)
* [Roadmaps](https://github.com/verdaccio/verdaccio/projects)
* [Reporting an issue](https://github.com/verdaccio/verdaccio/blob/master/CONTRIBUTING.md#reporting-a-bug)
* [Running discussions](https://github.com/verdaccio/verdaccio/issues?q=is%3Aissue+is%3Aopen+label%3Adiscuss)
* [Chat](http://chat.verdaccio.org/)
@@ -101,10 +101,6 @@ If you have any issue you can try the following options, do no desist to ask or
* [FAQ](https://github.com/verdaccio/verdaccio/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Aquestion%20)
* [Docker Examples](https://github.com/verdaccio/docker-examples)
### Translations
Translations are handled locally. I18n files can be found in the folder ```i18n/translations/*``` of this repository. We would love to provide translations from other languages, embracing all our users, but unfortunately we cannot do this without your help. Would you like to help us? Please feel **super welcome** to add a locale by opening a pull request.
### License
Verdaccio is [MIT licensed](https://github.com/verdaccio/verdaccio/blob/master/LICENSE)

View File

@@ -1,40 +0,0 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import translationEN from './translations/en-US.json';
import translationPT from './translations/pt-BR.json';
import translationES from './translations/es-ES.json';
import translationDE from './translations/de-DE.json';
i18n
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
// in case window.VEDACCIO_LANGUAGE is undefined,it will fall back to 'en-US'
lng: window?.__VERDACCIO_BASENAME_UI_OPTIONS?.language,
fallbackLng: 'en-US',
whitelist: ['en-US', 'pt-BR', 'es-ES', 'de-DE'],
load: 'currentOnly',
resources: {
'en-US': {
translation: translationEN,
},
'pt-BR': {
translation: translationPT,
},
'es-ES': {
translation: translationES,
},
'de-DE': {
translation: translationDE,
},
},
debug: false,
interpolation: {
escapeValue: false, // react already safes from xss
},
});
export default i18n;

View File

@@ -1,139 +0,0 @@
{
"copy-to-clipboard": "In die Zwischenablage kopieren",
"author-anonymous": "Anonymus",
"action-bar-action": {
"visit-home-page": "Zur Homepage",
"open-an-issue": "Einen Fehler melden",
"download-tarball": "Archiv (Tarball) herunterladen"
},
"dialog": {
"registry-info": {
"title": "Registrierungsinformationen"
}
},
"header": {
"documentation": "Dokumentation",
"registry-info": "Registrierungsinformationen",
"greetings": "Hallo "
},
"search": {
"packages": "Pakete suchen"
},
"auto-complete": {
"loading": "wird geladen...",
"no-results-found": "Kein Ergebnis gefunden"
},
"tab": {
"uplinks": "Uplinks",
"versions": "Versionen",
"dependencies": "Abhängigkeiten",
"readme": "Liesmich"
},
"uplinks": {
"title": "Uplinks",
"no-items": "{{name}} hat keine Uplinks."
},
"versions": {
"current-tags": "Aktuelle Tags",
"version-history": "Versionsgeschichte",
"not-available": "Nicht verfĂĽgbar"
},
"package": {
"published-on": "Veröffentlicht am {{time}} •",
"version": "v{{version}}",
"visit-home-page": "Zur Homepage",
"homepage": "Homepage",
"open-an-issue": "Einen Fehler melden",
"bugs": "Fehler",
"download": "{{what}} herunterladen",
"the-tar-file": "die tar-Datei",
"tarball": "Archiv (Tarball)"
},
"dependencies": {
"has-no-dependencies": "{{package}} hat keine Abhängigkeiten",
"dependency-block": "{{package}}@{{version}}"
},
"form": {
"username": "Benutzername",
"password": "Passwort"
},
"form-placeholder": {
"username": "Dein Benutzername",
"password": "Dein sicheres Passwort"
},
"form-validation": {
"required-field": "Dieses Feld ist erforderlich",
"required-min-length": "Dieses Feld erfordert eine Mindestlänge von {{length}}",
"unable-to-sign-in": "Anmeldung nicht möglich",
"username-or-password-cant-be-empty": "Benutzername und Passwort dĂĽrfen nicht leer sein!"
},
"help": {
"title": "Noch kein Paket publiziert.",
"sub-title": "Um dein erstes Paket einfach zu publizieren:",
"first-step": "1. Einloggen",
"first-step-command-line": "npm adduser --registry {{registryUrl}}",
"second-step": "2. Publizieren",
"second-step-command-line": "npm publish --registry {{registryUrl}}",
"third-step": "3. Diese Seite aktualisieren."
},
"sidebar": {
"detail": {
"latest-version": "Letzte v{{version}}",
"version": "v{{version}}"
},
"installation": {
"title": "Installierung",
"install-using-yarn": "Mit yarn installieren",
"install-using-yarn-command": "yarn add {{packageName}}",
"install-using-npm": "Mit npm installieren",
"install-using-npm-command": "npm install {{packageName}}",
"install-using-pnpm": "Mit pnpm installieren",
"install-using-pnpm-command": "pnpm install {{packageName}}"
},
"repository": {
"title": "Repository"
},
"author": {
"title": "Autor(in)"
},
"distribution": {
"title": "Neueste Distribution",
"license": "Lizenz",
"size": "Größe",
"file-count": "Anzahl der Dateien"
},
"maintainers": {
"title": "Maintainer"
},
"contributors": {
"title": "Contributor"
},
"engines": {
"npm-version": "NPM Version",
"node-js": "NODE JS"
}
},
"footer": {
"powered-by": "UnterstĂĽtzt von",
"made-with-love-on": "Gemacht mit <0>♥</0> in"
},
"button": {
"close": "SchlieĂźen",
"cancel": "Abbrechen",
"login": "Einloggen",
"logout": "Ausloggen",
"go-to-the-home-page": "Zur Homepage",
"learn-more": "Mehr erfahren",
"fund-this-package": "Dieses Paket <0>finanzieren</0>"
},
"error": {
"unspecific": "Etwas ist schief gelaufen.",
"404": {
"page-not-found": "404 - Seite nicht gefunden",
"sorry-we-could-not-find-it": "Entschuldigung, wir konnten es nicht finden..."
},
"app-context-not-correct-used": "Der App-Kontext wurde nicht korrekt verwendet",
"theme-context-not-correct-used": "Der Theme-Kontext wurde nicht korrekt verwendet",
"package-meta-is-required-at-detail-context": "packageMeta wird bei DetailContext benötigt"
}
}

View File

@@ -1,139 +0,0 @@
{
"copy-to-clipboard": "Copy to clipboard",
"author-anonymous": "Anonymous",
"action-bar-action": {
"visit-home-page": "Visit homepage",
"open-an-issue": "Open an issue",
"download-tarball": "Download tarball"
},
"dialog": {
"registry-info": {
"title": "Register Info"
}
},
"header": {
"documentation": "Documentation",
"registry-info": "Registry Information",
"greetings": "Hi "
},
"search": {
"packages": "Search Packages"
},
"auto-complete": {
"loading": "Loading...",
"no-results-found": "No results found"
},
"tab": {
"uplinks": "Uplinks",
"versions": "Versions",
"dependencies": "Dependencies",
"readme": "Readme"
},
"uplinks": {
"title": "Uplinks",
"no-items": "{{name}} has no uplinks."
},
"versions": {
"current-tags": "Current Tags",
"version-history": "Version history",
"not-available": "Not available"
},
"package": {
"published-on": "Published on {{time}} •",
"version": "v{{version}}",
"visit-home-page": "Visit homepage",
"homepage": "Homepage",
"open-an-issue": "Open an issue",
"bugs": "Bugs",
"download": "Download {{what}}",
"the-tar-file": "the tar file",
"tarball": "Tarball"
},
"dependencies": {
"has-no-dependencies": "{{package}} has no dependencies.",
"dependency-block": "{{package}}@{{version}}"
},
"form": {
"username": "Username",
"password": "Password"
},
"form-placeholder": {
"username": "Your username",
"password": "Your strong password"
},
"form-validation": {
"required-field": "This field is required",
"required-min-length": "This field required the min length of {{length}}",
"unable-to-sign-in": "Unable to sign in",
"username-or-password-cant-be-empty": "Username or password can't be empty!"
},
"help": {
"title": "No Package Published Yet.",
"sub-title": "To publish your first package just:",
"first-step": "1. Login",
"first-step-command-line": "npm adduser --registry {{registryUrl}}",
"second-step": "2. Publish",
"second-step-command-line": "npm publish --registry {{registryUrl}}",
"third-step": "3. Refresh this page."
},
"sidebar": {
"detail": {
"latest-version": "Latest v{{version}}",
"version": "v{{version}}"
},
"installation": {
"title": "Installation",
"install-using-yarn": "Install using yarn",
"install-using-yarn-command": "yarn add {{packageName}}",
"install-using-npm": "Install using npm",
"install-using-npm-command": "npm install {{packageName}}",
"install-using-pnpm": "Install using pnpm",
"install-using-pnpm-command": "pnpm install {{packageName}}"
},
"repository": {
"title": "Repository"
},
"author": {
"title": "Author"
},
"distribution": {
"title": "Latest Distribution",
"license": "License",
"size": "Size",
"file-count": "file count"
},
"maintainers": {
"title": "Maintainers"
},
"contributors": {
"title": "Contributors"
},
"engines": {
"npm-version": "NPM Version",
"node-js": "NODE JS"
}
},
"footer": {
"powered-by": "Powered by",
"made-with-love-on": "Made with <0>♥</0> on"
},
"button": {
"close": "Close",
"cancel": "Cancel",
"login": "Login",
"logout": "Logout",
"go-to-the-home-page": "Go to the home page",
"learn-more": "Learn More",
"fund-this-package": "<0>Fund</0> this package"
},
"error": {
"unspecific": "Something went wrong.",
"404": {
"page-not-found": "404 - Page not found",
"sorry-we-could-not-find-it": "Sorry, we couldn't find it..."
},
"app-context-not-correct-used": "The app context was not correct used",
"theme-context-not-correct-used": "The theme context was not correct used",
"package-meta-is-required-at-detail-context": "packageMeta is required at DetailContext"
}
}

View File

@@ -1,139 +0,0 @@
{
"copy-to-clipboard": "Copiar al portapapeles",
"author-anonymous": "AnĂłnimo",
"action-bar-action": {
"visit-home-page": "Visitar página principal",
"open-an-issue": "Reportar un error",
"download-tarball": "Descargar libreria"
},
"dialog": {
"registry-info": {
"title": "InformaciĂłn del Registro"
}
},
"header": {
"documentation": "DocumentaciĂłn",
"registry-info": "InformaciĂłn del Registro",
"greetings": "Hola "
},
"search": {
"packages": "Buscar paquetes"
},
"auto-complete": {
"loading": "Cargando...",
"no-results-found": "Sin resultados encontrados"
},
"tab": {
"uplinks": "Remoto",
"versions": "Versiones",
"dependencies": "Dependencias",
"readme": "Léeme"
},
"uplinks": {
"title": "Remoto",
"no-items": "{{name}} not tiene remotos."
},
"versions": {
"current-tags": "Etiquetas actuales",
"version-history": "Historial de versiones",
"not-available": "No disponible"
},
"package": {
"published-on": "Publicado en {{time}} •",
"version": "v{{version}}",
"visit-home-page": "Ir a la página principal",
"homepage": "Página pricinpal",
"open-an-issue": "Reportar un problema",
"bugs": "Errores",
"download": "Descargar {{what}}",
"the-tar-file": "el archivo tar",
"tarball": "Libreria"
},
"dependencies": {
"has-no-dependencies": "{{package}} no tiene dependencias.",
"dependency-block": "{{package}}@{{version}}"
},
"form": {
"username": "Usuario",
"password": "Contraseña"
},
"form-placeholder": {
"username": "Tu usuario",
"password": "Tu fuerte conntraseña"
},
"form-validation": {
"required-field": "Este campo es requerido",
"required-min-length": "Este campo es requerido y la mĂ­nima longitud es {{length}}",
"unable-to-sign-in": "No se ha podido iniciar sesiĂłn",
"username-or-password-cant-be-empty": "Nombre de usuario o contraseña no puede estar vacio!"
},
"help": {
"title": "NingĂşn paquete ha sido publicado aun.",
"sub-title": "Para publicar tu primer paquete:",
"first-step": "1. Inicia sesiĂłn",
"first-step-command-line": "npm adduser --registry {{registryUrl}}",
"second-step": "2. Publica",
"second-step-command-line": "npm publish --registry {{registryUrl}}",
"third-step": "3. Refresca la página."
},
"sidebar": {
"detail": {
"latest-version": "Ăšltima v{{version}}",
"version": "v{{version}}"
},
"installation": {
"title": "InstalaciĂłn",
"install-using-yarn": "Instala usando yarn",
"install-using-yarn-command": "yarn add {{packageName}}",
"install-using-npm": "Instala usando npm",
"install-using-npm-command": "npm install {{packageName}}",
"install-using-pnpm": "Instala usando pnpm",
"install-using-pnpm-command": "pnpm install {{packageName}}"
},
"repository": {
"title": "Repositorio"
},
"author": {
"title": "Autor"
},
"distribution": {
"title": "Ăšltima distribuciĂłn",
"license": "Licencia",
"size": "Tamaño",
"file-count": "archivo cuenta"
},
"maintainers": {
"title": "Mantenedores"
},
"contributors": {
"title": "Colaboradores"
},
"engines": {
"npm-version": "Version NPM",
"node-js": "NODE JS"
}
},
"footer": {
"powered-by": "Hecho con",
"made-with-love-on": "Hecho con <0>♥</0> on"
},
"button": {
"close": "Cerrar",
"cancel": "Cancelar",
"login": "Iniciar sesiĂłn",
"logout": "Cerrar sesiĂłn",
"go-to-the-home-page": "Ir a la página principal",
"learn-more": "Aprender más",
"fund-this-package": "<0>Donar</0> a este paquete"
},
"error": {
"unspecific": "Algo ha salido mal.",
"404": {
"page-not-found": "404 - Paquete no encontrado",
"sorry-we-could-not-find-it": "Lo siento, no hemos podido encontrarlo..."
},
"app-context-not-correct-used": "El contexto de la aplicaciĂłn no fue correctamente usado",
"theme-context-not-correct-used": "El contexto del tema no fue correctamente usado",
"package-meta-is-required-at-detail-context": "packageMeta es requerido en DetailContext"
}
}

View File

@@ -1,139 +0,0 @@
{
"copy-to-clipboard": "Copiar para área de transferência",
"author-anonymous": "AnĂ´nimo(a)",
"action-bar-action": {
"visit-home-page": "Visitar a página inicial",
"open-an-issue": "Criar um incidente",
"download-tarball": "Baixar Tarball"
},
"dialog": {
"registry-info": {
"title": "Informações do Registro"
}
},
"header": {
"documentation": "Documentação",
"registry-info": "Informações do Registro",
"greetings": "Oi "
},
"search": {
"packages": "Pesquisar Pacotes"
},
"auto-complete": {
"loading": "Carregando...",
"no-results-found": "Nenhum resultado encontrado"
},
"tab": {
"uplinks": "Uplinks",
"versions": "Versões",
"dependencies": "DependĂŞncias",
"readme": "Leia-me"
},
"uplinks": {
"title": "Uplinks",
"no-items": "{{name}} nĂŁo tem uplinks."
},
"versions": {
"current-tags": "Tags atuais",
"version-history": "Histórico de versões",
"not-available": "NĂŁo disponĂ­vel"
},
"package": {
"published-on": "Publicado em {{time}} •",
"version": "v{{version}}",
"visit-home-page": "Visitar a página inicial",
"homepage": "Página inicial",
"open-an-issue": "Criar um incidente",
"bugs": "Erros",
"download": "Baixar {{what}}",
"the-tar-file": "o arquivo tar",
"tarball": "Tarball"
},
"dependencies": {
"has-no-dependencies": "{{package}} nĂŁo tem dependĂŞncias.",
"dependency-block": "{{package}}@{{version}}"
},
"form": {
"username": "Nome do usuário",
"password": "Senha"
},
"form-placeholder": {
"username": "O seu nome",
"password": "A sua senha forte"
},
"form-validation": {
"required-field": "Este campo Ă© obrigatĂłrio",
"required-min-length": "Este campo requer o mĂ­nimo de {{length}} caracteres",
"unable-to-sign-in": "NĂŁo foi possĂ­vel fazer login",
"username-or-password-cant-be-empty": "Nome de usuário ou senha não podem estar vazios!"
},
"help": {
"title": "Nenhum pacote publicado ainda.",
"sub-title": "Para publicar seu primeiro pacote apenas:",
"first-step": "1. Faça login",
"first-step-command-line": "npm adduser --registry {{registryUrl}}",
"second-step": "2. Publique",
"second-step-command-line": "npm publish --registry {{registryUrl}}",
"third-step": "3. Atualize esta página."
},
"sidebar": {
"detail": {
"latest-version": "Ăšltima versĂŁo: v{{version}}",
"version": "v{{version}}"
},
"installation": {
"title": "Instalação",
"install-using-yarn": "Instale usando yarn",
"install-using-yarn-command": "yarn add {{packageName}}",
"install-using-npm": "Instale usando npm",
"install-using-npm-command": "npm install {{packageName}}",
"install-using-pnpm": "Instale usando pnpm",
"install-using-pnpm-command": "pnpm install {{packageName}}"
},
"repository": {
"title": "RepositĂłrio"
},
"author": {
"title": "Autor(a)"
},
"distribution": {
"title": "Distribuição mais recente",
"license": "Licença",
"size": "Tamanho",
"file-count": "Contagem de arquivos"
},
"maintainers": {
"title": "Mantenedores(as)"
},
"contributors": {
"title": "Contribuidores(as)"
},
"engines": {
"npm-version": "VersĂŁo NPM",
"node-js": "NODE JS"
}
},
"footer": {
"powered-by": "DistribuĂ­do por",
"made-with-love-on": "Feito com amor <0>♥</0> no(a)"
},
"button": {
"close": "Fechar",
"cancel": "Cancelar",
"login": "Conectar",
"logout": "Desconectar",
"go-to-the-home-page": "Ir para a página inicial",
"learn-more": "Leia mais",
"fund-this-package": "<0>Financie</0> este pacote"
},
"error": {
"unspecific": "Algo deu errado.",
"404": {
"page-not-found": "404 - Página não encontrada",
"sorry-we-could-not-find-it": "Desculpe, nĂŁo conseguimos encontrar..."
},
"app-context-not-correct-used": "O contexto do aplicativo nĂŁo foi usado corretamente",
"theme-context-not-correct-used": "O contexto do tema nĂŁo foi usado corretamente",
"package-meta-is-required-at-detail-context": "packageMeta Ă© requerido em DetailContext"
}
}

View File

@@ -1 +1 @@
jest.requireActual('babel/polyfill');
require.requireActual('babel/polyfill');

View File

@@ -6,15 +6,15 @@ import 'raf/polyfill';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { GlobalWithFetchMock } from 'jest-fetch-mock';
import 'mutationobserver-shim';
// @ts-ignore : Only a void function can be called with the 'new' keyword
configure({ adapter: new Adapter() });
// @ts-ignore : Property '__APP_VERSION__' does not exist on type 'Global'.
global.__APP_VERSION__ = '1.0.0';
// @ts-ignore : Property '__VERDACCIO_BASENAME_UI_OPTIONS' does not exist on type 'Global'.
global.__VERDACCIO_BASENAME_UI_OPTIONS = { base: 'http://localhost' };
// @ts-ignore : Property 'VERDACCIO_API_URL' does not exist on type 'Global'.
global.__VERDACCIO_BASENAME_UI_OPTIONS = {};
global.VERDACCIO_API_URL = 'https://verdaccio.tld';
const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;

View File

@@ -0,0 +1,5 @@
{
"rules": {
"@typescript-eslint/explicit-function-return-type": 0
}
}

View File

@@ -3,19 +3,12 @@
*/
import { Base64 } from 'js-base64';
import dayjs from 'dayjs';
import addHours from 'date-fns/addHours';
export function generateTokenWithTimeRange(amount = 0) {
export function generateTokenWithTimeRange(limit = 0) {
const payload = {
username: 'verdaccio',
exp: Number.parseInt(
String(
dayjs(new Date())
.add(amount, 'hour')
.valueOf() / 1000
),
10
),
exp: Number.parseInt(String(addHours(new Date(), limit).getTime() / 1000), 10),
};
return `xxxxxx.${Base64.encode(JSON.stringify(payload))}.xxxxxx`;
}

View File

@@ -192,7 +192,7 @@ export const packageMeta = {
jest: { snapshotSerializers: ['jest-serializer-enzyme'] },
engines: { node: '>=4.6.1', npm: '>=2.15.9' },
preferGlobal: true,
publishConfig: { registry: 'https://registry.verdaccio.org' },
publishConfig: { registry: 'http://localhost:4873/' },
license: 'WTFPL',
contributors: [
{
@@ -578,7 +578,7 @@ export const packageMeta = {
_npmUser: {},
dist: {
shasum: '958c919180e7f2ed6775f48d4ec64bd8de2a14df',
tarball: 'https://registry.verdaccio.org/verdaccio/-/verdaccio-2.7.1.tgz',
tarball: 'http://localhost:4873/verdaccio/-/verdaccio-2.7.1.tgz',
},
},
};

View File

@@ -1,6 +1,6 @@
{
"name": "@verdaccio/ui-theme",
"version": "1.3.0",
"version": "0.3.6",
"description": "Verdaccio User Interface",
"author": {
"name": "Verdaccio Core Team",
@@ -13,82 +13,77 @@
"homepage": "https://verdaccio.org",
"main": "index.js",
"devDependencies": {
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.0",
"@babel/plugin-proposal-optional-chaining": "7.8.0",
"@commitlint/cli": "8.3.5",
"@commitlint/config-conventional": "8.3.4",
"@commitlint/cli": "8.2.0",
"@commitlint/config-conventional": "8.2.0",
"@emotion/core": "10.0.22",
"@emotion/styled": "10.0.23",
"@material-ui/core": "4.8.0",
"@material-ui/core": "4.6.1",
"@material-ui/icons": "4.5.1",
"@octokit/rest": "16.35.2",
"@testing-library/jest-dom": "4.2.4",
"@testing-library/react": "9.4.0",
"@octokit/rest": "16.35.0",
"@testing-library/react": "9.3.2",
"@types/autosuggest-highlight": "3.1.0",
"@types/enzyme": "3.10.4",
"@types/jest": "24.0.24",
"@types/enzyme": "3.10.3",
"@types/jest": "24.0.23",
"@types/js-base64": "2.3.1",
"@types/lodash": "4.14.149",
"@types/node": "13.1.6",
"@types/react": "16.9.17",
"@types/node": "12.12.11",
"@types/react": "16.9.11",
"@types/react-autosuggest": "9.3.13",
"@types/react-dom": "16.9.4",
"@types/react-router-dom": "5.1.3",
"@types/request": "2.48.4",
"@types/validator": "12.0.1",
"@types/webpack-env": "1.15.0",
"@typescript-eslint/parser": "2.18.0",
"@verdaccio/babel-preset": "9.0.0",
"@verdaccio/commons-api": "9.0.0",
"@verdaccio/eslint-config": "8.4.2",
"@verdaccio/types": "9.0.0",
"@types/react-router-dom": "5.1.2",
"@types/request": "2.48.3",
"@types/validator": "12.0.0",
"@types/webpack-env": "1.14.1",
"@typescript-eslint/parser": "2.8.0",
"@verdaccio/babel-preset": "8.2.0",
"@verdaccio/commons-api": "8.3.0",
"@verdaccio/eslint-config": "8.2.0",
"@verdaccio/types": "8.3.0",
"autosuggest-highlight": "3.1.1",
"babel-loader": "8.0.6",
"bundlesize": "0.18.0",
"codeceptjs": "2.4.0",
"codecov": "3.6.5",
"concurrently": "5.0.2",
"codeceptjs": "2.3.5",
"codecov": "3.6.1",
"concurrently": "5.0.0",
"cross-env": "6.0.3",
"css-loader": "3.4.2",
"dayjs": "1.8.19",
"css-loader": "3.2.0",
"date-fns": "2.8.1",
"detect-secrets": "1.0.5",
"emotion": "10.0.27",
"emotion-theming": "10.0.27",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.2",
"emotion": "10.0.23",
"emotion-theming": "10.0.19",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.1",
"enzyme-to-json": "3.4.3",
"eslint": "6.7.2",
"eslint": "6.6.0",
"eslint-plugin-codeceptjs": "1.2.0",
"eslint-plugin-import": "2.19.1",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-prettier": "3.1.2",
"eslint-plugin-react": "7.17.0",
"eslint-plugin-prettier": "3.1.1",
"eslint-plugin-react": "7.16.0",
"eslint-plugin-react-hooks": "2.3.0",
"eslint-plugin-verdaccio": "8.4.2",
"file-loader": "5.0.2",
"eslint-plugin-verdaccio": "8.2.0",
"file-loader": "4.3.0",
"friendly-errors-webpack-plugin": "1.7.0",
"get-stdin": "7.0.0",
"github-markdown-css": "3.0.1",
"html-webpack-plugin": "3.2.0",
"husky": "3.1.0",
"i18next": "19.1.0",
"identity-obj-proxy": "3.0.0",
"in-publish": "2.0.0",
"jest": "24.9.0",
"jest-emotion": "10.0.27",
"jest-emotion": "10.0.17",
"jest-environment-jsdom": "24.9.0",
"jest-environment-jsdom-global": "1.2.0",
"jest-environment-node": "25.1.0",
"jest-fetch-mock": "3.0.1",
"jest-environment-node": "24.9.0",
"jest-fetch-mock": "2.1.2",
"js-base64": "2.5.1",
"js-yaml": "3.13.1",
"lint-staged": "9.5.0",
"lint-staged": "8.2.1",
"localstorage-memory": "1.0.3",
"lockfile-lint": "3.0.5",
"lockfile-lint": "2.2.0",
"lodash": "^4.17.15",
"mini-css-extract-plugin": "0.9.0",
"mutationobserver-shim": "0.3.3",
"node-mocks-http": "1.8.1",
"mini-css-extract-plugin": "0.8.0",
"node-mocks-http": "1.8.0",
"normalize.css": "8.0.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
"ora": "4.0.3",
@@ -97,37 +92,35 @@
"puppeteer": "2.0.0",
"react": "16.12.0",
"react-autosuggest": "9.4.3",
"react-dom": "16.13.0",
"react-hook-form": "3.29.4",
"react-dom": "16.12.0",
"react-hot-loader": "4.12.18",
"react-i18next": "11.3.1",
"react-router-dom": "5.1.2",
"request": "2.88.0",
"resolve-url-loader": "3.1.1",
"rimraf": "3.0.0",
"source-map-loader": "0.2.4",
"standard-version": "7.0.1",
"style-loader": "1.1.2",
"style-loader": "1.0.0",
"stylelint": "12.0.0",
"stylelint-config-recommended": "3.0.0",
"stylelint-config-styled-components": "0.1.1",
"stylelint-processor-styled-components": "1.9.0",
"stylelint-webpack-plugin": "1.1.2",
"stylelint-processor-styled-components": "1.8.0",
"stylelint-webpack-plugin": "1.1.0",
"supertest": "4.0.2",
"typeface-roboto": "0.0.75",
"typescript": "3.7.4",
"typescript": "3.7.2",
"uglifyjs-webpack-plugin": "2.2.0",
"url-loader": "3.0.0",
"url-loader": "2.3.0",
"validator": "12.1.0",
"verdaccio": "4.4.2",
"verdaccio-auth-memory": "9.0.0",
"verdaccio-memory": "9.0.0",
"verdaccio": "4.3.5",
"verdaccio-auth-memory": "8.3.0",
"verdaccio-memory": "8.3.0",
"wait-on": "3.3.0",
"webpack": "4.41.5",
"webpack": "4.41.2",
"webpack-bundle-analyzer": "3.6.0",
"webpack-bundle-size-analyzer": "3.1.0",
"webpack-cli": "3.3.10",
"webpack-dev-server": "3.10.1",
"webpack-dev-server": "3.9.0",
"webpack-merge": "4.2.2",
"whatwg-fetch": "3.0.0",
"xss": "1.0.6"
@@ -140,7 +133,7 @@
"bundlesize": [
{
"path": "./static/vendors.*.js",
"maxSize": "200 kB"
"maxSize": "180 kB"
},
{
"path": "./static/main.*.js",
@@ -173,7 +166,6 @@
"test:acceptance:server": "concurrently --kill-others \"npm run verdaccio:server\" \"npm run test:acceptance\"",
"test:e2e": "cross-env BABEL_ENV=test jest --config ./test/jest.config.e2e.js",
"test": "cross-env NODE_ENV=test BABEL_ENV=test TZ=UTC jest --config ./jest/jest.config.js --maxWorkers 2 --passWithNoTests",
"test:update-snapshot": "npm run test -- -u",
"test:size": "bundlesize",
"lint": "npm run lint:js && npm run lint:css && npm run lint:lockfile",
"lint:js": "npm run type-check && eslint . --ext .js,.ts,.tsx",
@@ -189,23 +181,29 @@
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run verdaccio:server\""
},
"engines": {
"node": ">= 8",
"node": ">=8",
"npm": ">=5"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged --relative",
"pre-commit": "lint-staged",
"commit-msg": "commitlint -e $GIT_PARAMS"
}
},
"lint-staged": {
"*.{js,tsx,ts}": [
"eslint . --ext .js,.ts,.tsx",
"prettier --write"
],
"*": [
"detect-secrets-launcher --baseline .secrets-baseline",
"git add"
"relative": true,
"linters": {
"*.{js,tsx,ts}": [
"eslint . --ext .js,.ts,.tsx",
"prettier --write"
],
"*": [
"detect-secrets-launcher --baseline .secrets-baseline",
"git add"
]
},
"ignore": [
"*.json"
]
},
"license": "MIT",
@@ -218,5 +216,6 @@
"type": "opencollective",
"url": "https://opencollective.com/verdaccio",
"logo": "https://opencollective.com/verdaccio/logo.txt"
}
},
"dependencies": {}
}

View File

@@ -4976,12 +4976,8 @@
"_attachments": {
"jquery-1.5.1.tgz": {
"shasum": "2ae2d661e906c1a01e044a71bb5b2743942183e5"
},
"jquery-3.3.1.tgz": {
"shasum": "958ce29e81c9790f31be7792df5d4d95fc57fbca"
}
},
"_rev": "61-e6be890a78963127",
"readme": "# jQuery\n\n> jQuery is a fast, small, and feature-rich JavaScript library.\n\nFor information on how to get started and how to use jQuery, please see [jQuery's documentation](http://api.jquery.com/).\nFor source files and issues, please visit the [jQuery repo](https://github.com/jquery/jquery).\n\nIf upgrading, please see the [blog post for 3.3.1](https://blog.jquery.com/2017/03/20/jquery-3.3.1-now-available/). This includes notable differences from the previous version and a more readable changelog.\n\n## Including jQuery\n\nBelow are some of the most common ways to include jQuery.\n\n### Browser\n\n#### Script tag\n\n```html\n<script src=\"https://code.jquery.com/jquery-3.3.1.min.js\"></script>\n```\n\n#### Babel\n\n[Babel](http://babeljs.io/) is a next generation JavaScript compiler. One of the features is the ability to use ES6/ES2015 modules now, even though browsers do not yet support this feature natively.\n\n```js\nimport $ from \"jquery\";\n```\n\n#### Browserify/Webpack\n\nThere are several ways to use [Browserify](http://browserify.org/) and [Webpack](https://webpack.github.io/). For more information on using these tools, please refer to the corresponding project's documention. In the script, including jQuery will usually look like this...\n\n```js\nvar $ = require(\"jquery\");\n```\n\n#### AMD (Asynchronous Module Definition)\n\nAMD is a module format built for the browser. For more information, we recommend [require.js' documentation](http://requirejs.org/docs/whyamd.html).\n\n```js\ndefine([\"jquery\"], function($) {\n\n});\n```\n\n### Node\n\nTo include jQuery in [Node](nodejs.org), first install with npm.\n\n```sh\nnpm install jquery\n```\n\nFor jQuery to work in Node, a window with a document is required. Since no such window exists natively in Node, one can be mocked by tools such as [jsdom](https://github.com/tmpvar/jsdom). This can be useful for testing purposes.\n\n```js\nrequire(\"jsdom\").env(\"\", function(err, window) {\n\tif (err) {\n\t\tconsole.error(err);\n\t\treturn;\n\t}\n\n\tvar $ = require(\"jquery\")(window);\n});\n```",
"_id": "jquery"
}
"_rev": "60-fed4915c27b9c1e6",
"readme": "# jQuery\n\n> jQuery is a fast, small, and feature-rich JavaScript library.\n\nFor information on how to get started and how to use jQuery, please see [jQuery's documentation](http://api.jquery.com/).\nFor source files and issues, please visit the [jQuery repo](https://github.com/jquery/jquery).\n\nIf upgrading, please see the [blog post for 3.3.1](https://blog.jquery.com/2017/03/20/jquery-3.3.1-now-available/). This includes notable differences from the previous version and a more readable changelog.\n\n## Including jQuery\n\nBelow are some of the most common ways to include jQuery.\n\n### Browser\n\n#### Script tag\n\n```html\n<script src=\"https://code.jquery.com/jquery-3.3.1.min.js\"></script>\n```\n\n#### Babel\n\n[Babel](http://babeljs.io/) is a next generation JavaScript compiler. One of the features is the ability to use ES6/ES2015 modules now, even though browsers do not yet support this feature natively.\n\n```js\nimport $ from \"jquery\";\n```\n\n#### Browserify/Webpack\n\nThere are several ways to use [Browserify](http://browserify.org/) and [Webpack](https://webpack.github.io/). For more information on using these tools, please refer to the corresponding project's documention. In the script, including jQuery will usually look like this...\n\n```js\nvar $ = require(\"jquery\");\n```\n\n#### AMD (Asynchronous Module Definition)\n\nAMD is a module format built for the browser. For more information, we recommend [require.js' documentation](http://requirejs.org/docs/whyamd.html).\n\n```js\ndefine([\"jquery\"], function($) {\n\n});\n```\n\n### Node\n\nTo include jQuery in [Node](nodejs.org), first install with npm.\n\n```sh\nnpm install jquery\n```\n\nFor jQuery to work in Node, a window with a document is required. Since no such window exists natively in Node, one can be mocked by tools such as [jsdom](https://github.com/tmpvar/jsdom). This can be useful for testing purposes.\n\n```js\nrequire(\"jsdom\").env(\"\", function(err, window) {\n\tif (err) {\n\t\tconsole.error(err);\n\t\treturn;\n\t}\n\n\tvar $ = require(\"jquery\")(window);\n});\n```"
}

View File

@@ -1,11 +1,12 @@
import React from 'react';
import { ReactWrapper } from 'enzyme';
import { render, waitForElement, fireEvent } from '../utils/test-react-testing-library';
import { mount } from '../utils/test-enzyme';
import storage from '../utils/storage';
// eslint-disable-next-line jest/no-mocks-import
import { generateTokenWithTimeRange } from '../../jest/unit/components/__mocks__/token';
import App from './App';
import { AppProps } from './AppContext';
jest.mock('../utils/storage', () => {
class LocalStorageMock {
@@ -30,75 +31,66 @@ jest.mock('../utils/storage', () => {
});
jest.mock('../utils/api', () => ({
// eslint-disable-next-line jest/no-mocks-import
request: require('../../jest/unit/components/__mocks__/api').default.request,
}));
/* eslint-disable react/jsx-no-bind*/
describe('<App />', () => {
test('should display the Loading component at the beginning ', () => {
const { container, queryByTestId } = render(<App />);
describe('App', () => {
let wrapper: ReactWrapper<{}, AppProps, App>;
expect(container.firstChild).toMatchSnapshot();
expect(queryByTestId('loading')).toBeTruthy();
beforeEach(() => {
wrapper = mount(<App />);
});
test('should display the Header component ', async () => {
const { container, queryByTestId } = render(<App />);
expect(container.firstChild).toMatchSnapshot();
expect(queryByTestId('loading')).toBeTruthy();
// wait for the Header component appearance and return the element
const headerElement = await waitForElement(() => queryByTestId('header'));
expect(headerElement).toBeTruthy();
});
test('handleLogout - logouts the user and clear localstorage', async () => {
storage.setItem('username', 'verdaccio');
storage.setItem('token', generateTokenWithTimeRange(24));
const { queryByTestId } = render(<App />);
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitForElement(() => queryByTestId('header--menu-accountcircle'));
expect(accountCircleElement).toBeTruthy();
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
// wait for the Button's logout element component appearance and return the element
const buttonLogoutElement = await waitForElement(() => queryByTestId('header--button-logout'));
expect(buttonLogoutElement).toBeTruthy();
if (buttonLogoutElement) {
fireEvent.click(buttonLogoutElement);
expect(queryByTestId('greetings-label')).toBeFalsy();
}
}
test('toggleLoginModal: should toggle the value in state', () => {
const { handleToggleLoginModal } = wrapper.instance();
expect(wrapper.state().showLoginModal).toBeFalsy();
handleToggleLoginModal();
expect(wrapper.state('showLoginModal')).toBeTruthy();
expect(wrapper.state('error')).toEqual(undefined);
});
test('isUserAlreadyLoggedIn: token already available in storage', async () => {
storage.setItem('username', 'verdaccio');
storage.setItem('token', generateTokenWithTimeRange(24));
const { isUserAlreadyLoggedIn } = wrapper.instance();
const { queryByTestId, queryAllByText } = render(<App />);
isUserAlreadyLoggedIn();
// wait for the Account's circle element component appearance and return the element
const accountCircleElement = await waitForElement(() => queryByTestId('header--menu-accountcircle'));
expect(accountCircleElement).toBeTruthy();
expect(wrapper.state('user').username).toEqual('verdaccio');
});
if (accountCircleElement) {
fireEvent.click(accountCircleElement);
test('handleLogout - logouts the user and clear localstorage', async () => {
const { handleLogout } = wrapper.instance();
storage.setItem('username', 'verdaccio');
storage.setItem('token', 'xxxx.TOKEN.xxxx');
// wait for the Greeting's label element component appearance and return the element
const greetingsLabelElement = await waitForElement(() => queryByTestId('greetings-label'));
expect(greetingsLabelElement).toBeTruthy();
await handleLogout();
expect(wrapper.state('user')).toEqual({});
expect(wrapper.state('isUserLoggedIn')).toBeFalsy();
});
if (greetingsLabelElement) {
expect(queryAllByText('verdaccio')).toBeTruthy();
}
}
test('handleDoLogin - login the user successfully', async () => {
const { handleDoLogin } = wrapper.instance();
await handleDoLogin('sam', '1234');
const result = {
username: 'sam',
};
expect(wrapper.state('isUserLoggedIn')).toBeTruthy();
expect(wrapper.state('showLoginModal')).toBeFalsy();
expect(storage.getItem('username')).toEqual('sam');
expect(storage.getItem('token')).toEqual('TEST_TOKEN');
expect(wrapper.state('user')).toEqual(result);
});
test('handleDoLogin - authentication failure', async () => {
const { handleDoLogin } = wrapper.instance();
await handleDoLogin('sam', '12345');
const result = {
description: 'bad username/password, access denied',
title: 'Unable to login',
type: 'error',
};
expect(wrapper.state('user')).toEqual({});
expect(wrapper.state('error')).toEqual(result);
});
});

View File

@@ -1,86 +1,184 @@
/* eslint-disable react/jsx-max-depth */
import React, { useState, useEffect, Suspense } from 'react';
import styled from '@emotion/styled';
import React, { Component, ReactElement } from 'react';
import isNil from 'lodash/isNil';
import { Router } from 'react-router-dom';
import '../../i18n/config';
import storage from '../utils/storage';
import { isTokenExpire } from '../utils/login';
import Header from '../components/Header';
import Footer from '../components/Footer';
import { makeLogin, isTokenExpire } from '../utils/login';
import Loading from '../components/Loading';
import Box from '../muiComponents/Box';
import StyleBaseline from '../design-tokens/StyleBaseline';
import { Theme } from '../design-tokens/theme';
import LoginModal from '../components/Login';
import Header from '../components/Header';
import { Container, Content } from '../components/Layout';
import API from '../utils/api';
import Footer from '../components/Footer';
import AppContextProvider from './AppContextProvider';
import AppRoute, { history } from './AppRoute';
import loadDayJSLocale from './load-dayjs-locale';
import AppRoute from './AppRoute';
import { AppProps, AppContextProvider } from './AppContext';
const StyledBox = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor: theme?.palette.background.default,
}));
const StyledBoxContent = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
[`@media screen and (min-width: ${theme && theme.breakPoints.container}px)`]: {
maxWidth: theme && theme.breakPoints.container,
width: '100%',
marginLeft: 'auto',
marginRight: 'auto',
},
}));
/* eslint-disable react/jsx-no-bind */
/* eslint-disable react-hooks/exhaustive-deps */
const App: React.FC = () => {
const [user, setUser] = useState();
/**
* Logout user
* Required by: <Header />
*/
const logout = () => {
storage.removeItem('username');
storage.removeItem('token');
setUser(undefined);
export default class App extends Component<{}, AppProps> {
public state: AppProps = {
logoUrl: window.VERDACCIO_LOGO,
user: {},
scope: window.VERDACCIO_SCOPE || '',
showLoginModal: false,
isUserLoggedIn: false,
packages: [],
isLoading: true,
};
const checkUserAlreadyLoggedIn = () => {
public componentDidMount(): void {
this.isUserAlreadyLoggedIn();
this.loadOnHandler();
}
// eslint-disable-next-line no-unused-vars
public componentDidUpdate(_: AppProps, prevState: AppProps): void {
const { isUserLoggedIn } = this.state;
if (prevState.isUserLoggedIn !== isUserLoggedIn) {
this.loadOnHandler();
}
}
public render(): React.ReactElement<HTMLDivElement> {
const { isLoading, isUserLoggedIn, packages, logoUrl, user, scope } = this.state;
const context = { isUserLoggedIn, packages, logoUrl, user, scope };
return (
<Container isLoading={isLoading}>
{isLoading ? <Loading /> : <AppContextProvider value={context}>{this.renderContent()}</AppContextProvider>}
{this.renderLoginModal()}
</Container>
);
}
public isUserAlreadyLoggedIn = () => {
// checks for token validity
const token = storage.getItem('token');
const username = storage.getItem('username');
const username: string = storage.getItem('username') as string;
if (isTokenExpire(token) || isNil(username)) {
logout();
return;
this.handleLogout();
} else {
this.setState({
user: { username },
isUserLoggedIn: true,
});
}
setUser({ username });
};
useEffect(() => {
checkUserAlreadyLoggedIn();
loadDayJSLocale();
}, []);
public loadOnHandler = async () => {
try {
const packages = await API.request<any[]>('packages', 'GET');
// @ts-ignore: FIX THIS TYPE: Type 'any[]' is not assignable to type '[]'
this.setState({
packages,
isLoading: false,
});
} catch (error) {
// FIXME: add dialog
console.error({
title: 'Warning',
message: `Unable to load package list: ${error.message}`,
});
this.setLoading(false);
}
};
return (
<Suspense fallback={<Loading />}>
<StyleBaseline />
<StyledBox display="flex" flexDirection="column" height="100%">
<>
<Router history={history}>
<AppContextProvider user={user}>
<Header />
<StyledBoxContent flexGrow={1}>
<AppRoute />
</StyledBoxContent>
</AppContextProvider>
</Router>
<Footer />
</>
</StyledBox>
</Suspense>
);
};
public setLoading = (isLoading: boolean) =>
this.setState({
isLoading,
});
export default App;
/**
* Toggles the login modal
* Required by: <LoginModal /> <Header />
*/
public handleToggleLoginModal = () => {
this.setState(prevState => ({
showLoginModal: !prevState.showLoginModal,
}));
};
/**
* handles login
* Required by: <Header />
*/
public handleDoLogin = async (usernameValue: string, passwordValue: string) => {
const { username, token, error } = await makeLogin(usernameValue, passwordValue);
if (username && token) {
storage.setItem('username', username);
storage.setItem('token', token);
this.setLoggedUser(username);
}
if (error) {
this.setState({
user: {},
error,
});
}
};
public setLoggedUser = (username: string) => {
this.setState({
user: {
username,
},
isUserLoggedIn: true, // close login modal after successful login
showLoginModal: false, // set isUserLoggedIn to true
});
};
/**
* Logouts user
* Required by: <Header />
*/
public handleLogout = () => {
storage.removeItem('username');
storage.removeItem('token');
this.setState({
user: {},
isUserLoggedIn: false,
});
};
public renderLoginModal = (): ReactElement<HTMLElement> => {
const { error, showLoginModal } = this.state;
return (
<LoginModal
error={error}
onCancel={this.handleToggleLoginModal}
onSubmit={this.handleDoLogin}
visibility={showLoginModal}
/>
);
};
public renderContent = (): ReactElement<HTMLElement> => {
return (
<>
<Content>
<AppRoute>{this.renderHeader()}</AppRoute>
</Content>
<Footer />
</>
);
};
public renderHeader = (): ReactElement<HTMLElement> => {
const {
logoUrl,
user: { username },
scope,
} = this.state;
return (
<Header
logo={logoUrl}
onLogout={this.handleLogout}
onToggleLoginModal={this.handleToggleLoginModal}
scope={scope}
username={username}
/>
);
};
}

View File

@@ -1,18 +0,0 @@
import { createContext } from 'react';
export interface AppProps {
user?: User;
scope: string;
}
export interface User {
username: string;
}
export interface AppContextProps extends AppProps {
setUser: (user?: User) => void;
}
const AppContext = createContext<undefined | AppContextProps>(undefined);
export default AppContext;

20
src/App/AppContext.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { createContext } from 'react';
import { FormError } from '../components/Login/Login';
export interface AppProps {
error?: FormError;
logoUrl: string;
user: {
username?: string;
};
scope: string;
showLoginModal: boolean;
isUserLoggedIn: boolean;
packages: [];
isLoading: boolean;
}
export const AppContext = createContext<Partial<AppProps>>({});
export const AppContextProvider = AppContext.Provider;
export const AppContextConsumer = AppContext.Consumer;

View File

@@ -1,41 +0,0 @@
import React, { useState, useEffect } from 'react';
import AppContext, { AppProps, User } from './AppContext';
interface Props {
user?: User;
}
/* eslint-disable react-hooks/exhaustive-deps */
const AppContextProvider: React.FC<Props> = ({ children, user }) => {
const [state, setState] = useState<AppProps>({
scope: window.VERDACCIO_SCOPE || '',
user,
});
useEffect(() => {
setState({
...state,
user,
});
}, [user]);
const setUser = (user?: User) => {
setState({
...state,
user,
});
};
return (
<AppContext.Provider
value={{
...state,
setUser,
}}>
{children}
</AppContext.Provider>
);
};
export default AppContextProvider;

View File

@@ -1,9 +1,10 @@
import React, { lazy, useContext } from 'react';
import React, { lazy, useContext, Suspense } from 'react';
import { Route as ReactRouterDomRoute, Switch, Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import { useTranslation } from 'react-i18next';
import AppContext from './AppContext';
import Loading from '../components/Loading';
import { AppContext } from './AppContext';
const NotFound = lazy(() => import('../components/NotFound'));
const VersionContextProvider = lazy(() => import('../pages/Version/VersionContextProvider'));
@@ -18,52 +19,48 @@ enum Route {
PACKAGE_VERSION = '/-/web/detail/:package/v/:version',
}
export const history = createBrowserHistory({
const history = createBrowserHistory({
basename: window.__VERDACCIO_BASENAME_UI_OPTIONS && window.__VERDACCIO_BASENAME_UI_OPTIONS.url_prefix,
});
const AppRoute: React.FC = () => {
/* eslint react/jsx-max-depth: 0 */
const AppRoute: React.FC = ({ children }) => {
const appContext = useContext(AppContext);
const { t } = useTranslation();
if (!appContext) {
throw Error(t('app-context-not-correct-used'));
}
const { user } = appContext;
const isUserLoggedIn = user && user.username;
const { isUserLoggedIn, packages } = appContext;
return (
<Router history={history}>
<Switch>
<ReactRouterDomRoute exact={true} path={Route.ROOT}>
<HomePage isUserLoggedIn={!!isUserLoggedIn} />
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE}>
<VersionContextProvider>
<VersionPage />
</VersionContextProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE_VERSION}>
<VersionContextProvider>
<VersionPage />
</VersionContextProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE_VERSION}>
<VersionContextProvider>
<VersionPage />
</VersionContextProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE}>
<VersionContextProvider>
<VersionPage />
</VersionContextProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute>
<NotFound />
</ReactRouterDomRoute>
</Switch>
<Suspense fallback={<Loading />}>
{children}
<Switch>
<ReactRouterDomRoute exact={true} path={Route.ROOT}>
<HomePage isUserLoggedIn={!!isUserLoggedIn} packages={packages || []} />
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE}>
<VersionContextProvider>
<VersionPage />
</VersionContextProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE_VERSION}>
<VersionContextProvider>
<VersionPage />
</VersionContextProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE_VERSION}>
<VersionContextProvider>
<VersionPage />
</VersionContextProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE}>
<VersionContextProvider>
<VersionPage />
</VersionContextProvider>
</ReactRouterDomRoute>
<ReactRouterDomRoute>
<NotFound />
</ReactRouterDomRoute>
</Switch>
</Suspense>
</Router>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1 @@
export { default } from './App';
export { default as AppContextProvider } from './AppContextProvider';

View File

@@ -1,52 +0,0 @@
import dayjs from 'dayjs';
import i18n from 'i18next';
function getFallFackLanguage(): string | undefined {
const fallbackLanguage = i18n.options.fallbackLng;
if (Array.isArray(fallbackLanguage)) {
return fallbackLanguage[0];
}
if (typeof fallbackLanguage === 'string') {
return fallbackLanguage;
}
return undefined;
}
function loadDayJSLocale() {
const fallbackLanguage = getFallFackLanguage();
const locale = i18n.language || fallbackLanguage;
// dayjs loades en-US by default
if (!locale || locale === 'en-US') {
return;
}
switch (locale.toLowerCase()) {
// At the moment we only support pt-BR, please see: i18n/translations/*
case 'pt-br':
{
require('dayjs/locale/pt-br');
dayjs.locale('pt-br');
}
break;
case 'de':
{
require('dayjs/locale/de');
dayjs.locale('de');
}
break;
case 'es-es':
{
require('dayjs/locale/es');
dayjs.locale('es');
}
break;
default:
break;
}
}
export default loadDayJSLocale;

18
src/App/styles.ts Normal file
View File

@@ -0,0 +1,18 @@
import { css } from '@emotion/core';
import { theme } from '../design-tokens/theme';
export const alertError = css({
backgroundColor: `${theme.palette.red} !important`,
minWidth: 'inherit !important',
});
export const alertErrorMsg = css({
display: 'flex',
alignItems: 'center',
});
export const alertIcon = css({
opacity: 0.9,
marginRight: '8px',
});

View File

@@ -1,71 +1,89 @@
import React from 'react';
import { render, cleanup } from '../../utils/test-react-testing-library';
import { DetailContext, DetailContextProps } from '../../pages/Version';
import { mount } from '../../utils/test-enzyme';
import api from '../../utils/api';
import ActionBar from './ActionBar';
import { ActionBar } from './ActionBar';
const detailContextValue: DetailContextProps = {
packageName: 'foo',
readMe: 'test',
enableLoading: () => {},
isLoading: false,
hasNotBeenFound: false,
packageMeta: {
_uplinks: {},
latest: {
name: '@verdaccio/local-storage',
version: '8.0.1-next.1',
dist: { fileCount: 0, unpackedSize: 0, tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz' },
homepage: 'https://verdaccio.org',
bugs: {
url: 'https://github.com/verdaccio/monorepo/issues',
},
const mockPackageMeta: jest.Mock = jest.fn(() => ({
latest: {
homepage: 'https://verdaccio.tld',
bugs: {
url: 'https://verdaccio.tld/bugs',
},
dist: {
tarball: 'https://verdaccio.tld/download',
},
},
};
}));
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({ contextValue }) => (
<DetailContext.Provider value={contextValue}>
<ActionBar />
</DetailContext.Provider>
);
jest.mock('../../pages/Version', () => ({
DetailContextConsumer: component => {
return component.children({ packageMeta: mockPackageMeta() });
},
}));
describe('<ActionBar /> component', () => {
afterEach(() => {
cleanup();
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
});
test('should render the component in default state', () => {
const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(container.firstChild).toMatchSnapshot();
const wrapper = mount(<ActionBar />);
expect(wrapper.html()).toMatchSnapshot();
});
test('when there is no action bar data', () => {
const packageMeta = {
...detailContextValue.packageMeta,
latest: {
...detailContextValue.packageMeta.latest,
homepage: undefined,
bugs: undefined,
dist: {
...detailContextValue.packageMeta.latest.dist,
tarball: undefined,
},
},
};
mockPackageMeta.mockImplementation(() => ({
latest: {},
}));
const { container } = render(<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />);
expect(container.firstChild).toMatchSnapshot();
const wrapper = mount(<ActionBar />);
// FIXME: this only renders the DetailContextConsumer, thus
// the wrapper will be always empty
expect(wrapper.html()).toEqual('');
});
test('when there is no latest property in package meta', () => {
mockPackageMeta.mockImplementation(() => ({}));
const wrapper = mount(<ActionBar />);
expect(wrapper.html()).toEqual('');
});
test('when there is a button to download a tarball', () => {
const { getByTitle } = render(<ComponentToBeRendered contextValue={{ ...detailContextValue }} />);
expect(getByTitle('Download tarball')).toBeTruthy();
mockPackageMeta.mockImplementation(() => ({
latest: {
dist: {
tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
},
},
}));
const wrapper = mount(<ActionBar />);
expect(wrapper.html()).toMatchSnapshot();
const button = wrapper.find('button');
expect(button).toHaveLength(1);
const spy = jest.spyOn(api, 'request');
button.simulate('click');
expect(spy).toHaveBeenCalled();
});
test('when there is a button to open an issue', () => {
const { getByTitle } = render(<ComponentToBeRendered contextValue={{ ...detailContextValue }} />);
expect(getByTitle('Open an issue')).toBeTruthy();
mockPackageMeta.mockImplementation(() => ({
latest: {
bugs: {
url: 'https://verdaccio.tld/bugs',
},
},
}));
const wrapper = mount(<ActionBar />);
expect(wrapper.html()).toMatchSnapshot();
const button = wrapper.find('button');
expect(button).toHaveLength(1);
});
});

View File

@@ -1,44 +1,133 @@
import React from 'react';
import React, { Component, ReactElement } from 'react';
import BugReportIcon from '@material-ui/icons/BugReport';
import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import { DetailContext } from '../../pages/Version';
import { isURL } from '../../utils/url';
import Box from '../../muiComponents/Box';
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/Version';
import { isURL, extractFileName, downloadFile } from '../../utils/url';
import api from '../../utils/api';
import Tooltip from '../../muiComponents/Tooltip';
import List from '../../muiComponents/List';
import ActionBarAction, { ActionBarActionProps } from './ActionBarAction';
import { Fab, ActionListItem } from './styles';
/* eslint-disable verdaccio/jsx-spread */
const ActionBar: React.FC = () => {
const detailContext = React.useContext(DetailContext);
export interface Action {
icon: string;
title: string;
handler?: Function;
}
const { packageMeta } = detailContext;
export async function downloadHandler(link: string): Promise<void> {
const fileStream: Blob = await api.request(link, 'GET', {
headers: {
['accept']:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
},
credentials: 'include',
});
const fileName = extractFileName(link);
downloadFile(fileStream, fileName);
}
if (!packageMeta?.latest) {
return null;
}
const { homepage, bugs, dist } = packageMeta.latest;
const actions: Array<ActionBarActionProps> = [];
if (homepage && isURL(homepage)) {
actions.push({ type: 'VISIT_HOMEPAGE', link: homepage });
}
if (bugs?.url && isURL(bugs.url)) {
actions.push({ type: 'OPEN_AN_ISSUE', link: bugs.url });
}
if (dist?.tarball && isURL(dist.tarball)) {
actions.push({ type: 'DOWNLOAD_TARBALL', link: dist.tarball });
}
return (
<Box alignItems="center" display="flex" marginBottom="8px">
{actions.map(action => (
<ActionBarAction key={action.link} {...action} />
))}
</Box>
);
const ACTIONS = {
homepage: {
icon: <HomeIcon />,
title: 'Visit homepage',
},
issue: {
icon: <BugReportIcon />,
title: 'Open an issue',
},
tarball: {
icon: <DownloadIcon />,
title: 'Download tarball',
handler: downloadHandler,
},
};
export default ActionBar;
class ActionBar extends Component {
public render(): ReactElement<HTMLElement> {
return (
<DetailContextConsumer>
{context => {
const { packageMeta } = context;
if (!packageMeta) {
return null;
}
return this.renderActionBar(context as VersionPageConsumerProps);
}}
</DetailContextConsumer>
);
}
private renderIconsWithLink(link: string, component: JSX.Element): ReactElement<HTMLElement> {
return (
<a href={link} target={'_blank'}>
{component}
</a>
);
}
private renderActionBar = ({ packageMeta }) => {
const { latest } = packageMeta;
if (!latest) {
return null;
}
const { homepage, bugs, dist } = latest;
const actionsMap = {
homepage,
issue: bugs ? bugs.url : null,
tarball: dist ? dist.tarball : null,
};
const renderList = Object.keys(actionsMap).reduce((component: React.ReactElement[], value, key) => {
const link = actionsMap[value];
if (link && isURL(link)) {
const actionItem: Action = ACTIONS[value];
if (actionItem.handler) {
const fab = (
<Tooltip key={key} title={actionItem['title']}>
<Fab
/* eslint-disable react/jsx-no-bind */
onClick={() => {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
actionItem.handler!(link);
}}
size={'small'}>
{actionItem['icon']}
</Fab>
</Tooltip>
);
component.push(fab);
} else {
const fab = <Fab size={'small'}>{actionItem['icon']}</Fab>;
component.push(
<Tooltip key={key} title={actionItem['title']}>
<>{this.renderIconsWithLink(link, fab)}</>
</Tooltip>
);
}
}
return component;
}, []);
if (renderList.length > 0) {
return (
<List>
<ActionListItem alignItems={'flex-start'} button={true}>
{renderList}
</ActionListItem>
</List>
);
}
return null;
};
}
export { ActionBar };

View File

@@ -1,63 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
import BugReportIcon from '@material-ui/icons/BugReport';
import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import { useTranslation } from 'react-i18next';
import Tooltip from '../../muiComponents/Tooltip';
import Link from '../Link';
import FloatingActionButton from '../../muiComponents/FloatingActionButton';
import { Theme } from '../../design-tokens/theme';
import downloadTarball from './download-tarball';
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>(props => ({
backgroundColor: props.theme && props.theme.palette.primary.main,
color: props.theme && props.theme.palette.white,
marginRight: 10,
}));
type ActionType = 'VISIT_HOMEPAGE' | 'OPEN_AN_ISSUE' | 'DOWNLOAD_TARBALL';
export interface ActionBarActionProps {
type: ActionType;
link: string;
}
/* eslint-disable react/jsx-no-bind */
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
const { t } = useTranslation();
switch (type) {
case 'VISIT_HOMEPAGE':
return (
<Tooltip title={t('action-bar-action.visit-home-page')}>
<Link external={true} to={link}>
<Fab size="small">
<HomeIcon />
</Fab>
</Link>
</Tooltip>
);
case 'OPEN_AN_ISSUE':
return (
<Tooltip title={t('action-bar-action.open-an-issue')}>
<Link external={true} to={link}>
<Fab size="small">
<BugReportIcon />
</Fab>
</Link>
</Tooltip>
);
case 'DOWNLOAD_TARBALL':
return (
<Tooltip title={t('action-bar-action.download-tarball')}>
<Fab data-testid="download-tarball-btn" onClick={downloadTarball(link)} size="small">
<DownloadIcon />
</Fab>
</Tooltip>
);
}
};
export default ActionBarAction;

View File

@@ -1,118 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ActionBar /> component should render the component in default state 1`] = `
.emotion-0 {
background-color: #4b5e40;
color: #fff;
margin-right: 10px;
}
exports[`<ActionBar /> component should render the component in default state 1`] = `""`;
<div
class="MuiBox-root MuiBox-root-2"
>
<a
class=""
href="https://verdaccio.org"
rel="noopener noreferrer"
target="_blank"
title="Visit homepage"
>
<h6
class="MuiTypography-root MuiTypography-subtitle1"
>
<button
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
tabindex="0"
type="button"
>
<span
class="MuiFab-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</h6>
</a>
<a
class=""
href="https://github.com/verdaccio/monorepo/issues"
rel="noopener noreferrer"
target="_blank"
title="Open an issue"
>
<h6
class="MuiTypography-root MuiTypography-subtitle1"
>
<button
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
tabindex="0"
type="button"
>
<span
class="MuiFab-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</h6>
</a>
<button
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
data-testid="download-tarball-btn"
tabindex="0"
title="Download tarball"
type="button"
>
<span
class="MuiFab-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
`;
exports[`<ActionBar /> component when there is a button to download a tarball 1`] = `"<ul class=\\"MuiList-root MuiList-padding\\"><div class=\\"MuiButtonBase-root MuiListItem-root css-l3mdff-ActionListItem eux6shq0 MuiListItem-gutters MuiListItem-button MuiListItem-alignItemsFlexStart\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><button class=\\"MuiButtonBase-root MuiFab-root css-is03ew-Fab eux6shq1 MuiFab-sizeSmall\\" tabindex=\\"0\\" type=\\"button\\" title=\\"Download tarball\\"><span class=\\"MuiFab-label\\"><svg class=\\"MuiSvgIcon-root\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z\\"></path></svg></span><span class=\\"MuiTouchRipple-root\\"></span></button><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<ActionBar /> component when there is no action bar data 1`] = `
<div
class="MuiBox-root MuiBox-root-77"
/>
`;
exports[`<ActionBar /> component when there is a button to open an issue 1`] = `"<ul class=\\"MuiList-root MuiList-padding\\"><div class=\\"MuiButtonBase-root MuiListItem-root css-l3mdff-ActionListItem eux6shq0 MuiListItem-gutters MuiListItem-button MuiListItem-alignItemsFlexStart\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><a href=\\"https://verdaccio.tld/bugs\\" target=\\"_blank\\"><button class=\\"MuiButtonBase-root MuiFab-root css-is03ew-Fab eux6shq1 MuiFab-sizeSmall\\" tabindex=\\"0\\" type=\\"button\\"><span class=\\"MuiFab-label\\"><svg class=\\"MuiSvgIcon-root\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z\\"></path></svg></span><span class=\\"MuiTouchRipple-root\\"></span></button></a><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;

View File

@@ -1,18 +0,0 @@
import api from '../../utils/api';
import { extractFileName, downloadFile } from '../../utils/url';
function downloadTarball(link: string) {
return async function downloadHandler(): Promise<void> {
const fileStream: Blob = await api.request(link, 'GET', {
headers: {
['accept']:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
},
credentials: 'include',
});
const fileName = extractFileName(link);
downloadFile(fileStream, fileName);
};
}
export default downloadTarball;

View File

@@ -1,2 +1 @@
export { default } from './ActionBar';
export { default as downloadTarball } from './download-tarball';

View File

@@ -0,0 +1,17 @@
import styled from '@emotion/styled';
import ListItem from '../../muiComponents/ListItem';
import FloatingActionButton from '../../muiComponents/FloatingActionButton';
import { Theme } from '../../design-tokens/theme';
export const ActionListItem = styled(ListItem)({
paddingTop: 0,
paddingLeft: 0,
paddingRight: 0,
});
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>(props => ({
backgroundColor: props.theme && props.theme.palette.primary.main,
color: props.theme && props.theme.palette.white,
marginRight: '10px',
}));

View File

@@ -1,17 +1,14 @@
import React, { FC, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { DetailContext } from '../../pages/Version';
import { isEmail } from '../../utils/url';
import Avatar from '../../muiComponents/Avatar';
import List from '../../muiComponents/List';
import { getAuthorName } from '../../utils/package';
import { StyledText, AuthorListItem, AuthorListItemText } from './styles';
const Author: FC = () => {
const { packageMeta } = useContext(DetailContext);
const { t } = useTranslation();
if (!packageMeta) {
return null;
@@ -28,7 +25,7 @@ const Author: FC = () => {
const avatarComponent = <Avatar alt={author.name} src={author.avatar} />;
return (
<List subheader={<StyledText variant={'subtitle1'}>{t('sidebar.author.title')}</StyledText>}>
<List subheader={<StyledText variant={'subtitle1'}>{'Author'}</StyledText>}>
<AuthorListItem button={true}>
{!email || !isEmail(email) ? (
avatarComponent
@@ -37,7 +34,8 @@ const Author: FC = () => {
{avatarComponent}
</a>
)}
{name && <AuthorListItemText primary={getAuthorName(name)} />}
<AuthorListItemText primary={name} />
</AuthorListItem>
</List>
);

View File

@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Author /> component should render the component in default state 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-5wp24z-StyledText e1xuehjw0 MuiTypography-subtitle1\\">sidebar.author.title</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1k45khb-AuthorListItem e1xuehjw1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><a href=\\"mailto:verdaccio.user@verdaccio.org?subject=verdaccio@4.0.0\\" target=\\"_top\\"><div class=\\"MuiAvatar-root MuiAvatar-circle\\"><img alt=\\"verdaccio user\\" src=\\"https://www.gravatar.com/avatar/000000\\" class=\\"MuiAvatar-img\\"></div></a><div class=\\"MuiListItemText-root css-1cnlq5d-AuthorListItemText e1xuehjw2\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">verdaccio user</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<Author /> component should render the component in default state 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText e1xuehjw0 MuiTypography-subtitle1\\">Author</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1k45khb-AuthorListItem e1xuehjw1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><a href=\\"mailto:verdaccio.user@verdaccio.org?subject=verdaccio@4.0.0\\" target=\\"_top\\"><div class=\\"MuiAvatar-root MuiAvatar-circle\\"><img alt=\\"verdaccio user\\" src=\\"https://www.gravatar.com/avatar/000000\\" class=\\"MuiAvatar-img\\"></div></a><div class=\\"MuiListItemText-root css-1cnlq5d-AuthorListItemText e1xuehjw2\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">verdaccio user</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<Author /> component should render the component when there is no author email 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-5wp24z-StyledText e1xuehjw0 MuiTypography-subtitle1\\">sidebar.author.title</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1k45khb-AuthorListItem e1xuehjw1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiAvatar-root MuiAvatar-circle\\"><img alt=\\"verdaccio user\\" src=\\"https://www.gravatar.com/avatar/000000\\" class=\\"MuiAvatar-img\\"></div><div class=\\"MuiListItemText-root css-1cnlq5d-AuthorListItemText e1xuehjw2\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">verdaccio user</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<Author /> component should render the component when there is no author email 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText e1xuehjw0 MuiTypography-subtitle1\\">Author</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1k45khb-AuthorListItem e1xuehjw1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiAvatar-root MuiAvatar-circle\\"><img alt=\\"verdaccio user\\" src=\\"https://www.gravatar.com/avatar/000000\\" class=\\"MuiAvatar-img\\"></div><div class=\\"MuiListItemText-root css-1cnlq5d-AuthorListItemText e1xuehjw2\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">verdaccio user</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;

View File

@@ -1,13 +1,14 @@
import styled from '@emotion/styled';
import { fontWeight } from '../../utils/styles/sizes';
import ListItem from '../../muiComponents/ListItem';
import Text from '../../muiComponents/Text';
import ListItemText from '../../muiComponents/ListItemText';
import { Theme } from '../../design-tokens/theme';
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
}));
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
textTransform: 'capitalize',
});
export const AuthorListItem = styled(ListItem)({
padding: 0,

View File

@@ -1,17 +1,16 @@
import React, { KeyboardEvent, memo } from 'react';
import React, { KeyboardEvent } from 'react';
import styled from '@emotion/styled';
import Autosuggest, { SuggestionSelectedEventData, InputProps, ChangeEvent } from 'react-autosuggest';
import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse';
import { useTranslation } from 'react-i18next';
import { fontWeight } from '../../utils/styles/sizes';
import MenuItem from '../../muiComponents/MenuItem';
import { Theme } from '../../design-tokens/theme';
import { Wrapper, InputField, SuggestionContainer } from './styles';
const StyledAnchor = styled('a')<{ highlight: boolean; theme?: Theme }>(props => ({
fontWeight: props.theme && props.highlight ? props.theme.fontWeight.semiBold : props.theme.fontWeight.light,
const StyledAnchor = styled('a')<{ fw: number }>(props => ({
fontWeight: props.fw,
}));
const StyledMenuItem = styled(MenuItem)({
@@ -65,8 +64,9 @@ const renderSuggestion = (suggestion, { query, isHighlighted }): JSX.Element =>
<StyledMenuItem component="div" selected={isHighlighted}>
<div>
{parts.map((part, index) => {
const fw = part.highlight ? fontWeight.semiBold : fontWeight.light;
return (
<StyledAnchor highlight={part.highlight} key={String(index)}>
<StyledAnchor fw={fw} key={String(index)}>
{part.text}
</StyledAnchor>
);
@@ -84,68 +84,70 @@ const renderMessage = (message): JSX.Element => {
);
};
const AutoComplete = memo(
({
const SUGGESTIONS_RESPONSE = {
LOADING: 'Loading...',
FAILURE: 'Something went wrong.',
NO_RESULT: 'No results found.',
};
const AutoComplete = ({
suggestions,
startAdornment,
onChange,
onSuggestionsFetch,
onCleanSuggestions,
value = '',
placeholder = '',
disableUnderline = false,
onClick,
onKeyDown,
onBlur,
suggestionsLoading = false,
suggestionsLoaded = false,
suggestionsError = false,
}: Props): JSX.Element => {
const autosuggestProps = {
renderInputComponent,
suggestions,
startAdornment,
getSuggestionValue,
renderSuggestion,
onSuggestionsFetchRequested: onSuggestionsFetch,
onSuggestionsClearRequested: onCleanSuggestions,
};
const inputProps: InputProps<unknown> = {
value,
onChange,
onSuggestionsFetch,
onCleanSuggestions,
value = '',
placeholder = '',
disableUnderline = false,
onClick,
placeholder,
// material-ui@4.5.1 introduce better types for TextInput, check readme
// @ts-ignore
startAdornment,
disableUnderline,
onKeyDown,
onBlur,
suggestionsLoading = false,
suggestionsLoaded = false,
suggestionsError = false,
}: Props) => {
const { t } = useTranslation();
const autosuggestProps = {
renderInputComponent,
suggestions,
getSuggestionValue,
renderSuggestion,
onSuggestionsFetchRequested: onSuggestionsFetch,
onSuggestionsClearRequested: onCleanSuggestions,
};
const inputProps: InputProps<unknown> = {
value,
onChange,
placeholder,
// material-ui@4.5.1 introduce better types for TextInput, check readme
// @ts-ignore
startAdornment,
disableUnderline,
onKeyDown,
onBlur,
};
// this format avoid arrow function eslint rule
function renderSuggestionsContainer({ containerProps, children, query }): JSX.Element {
return (
<SuggestionContainer {...containerProps} square={true}>
{suggestionsLoaded && children === null && query && renderMessage(t('auto-complete.no-results-found'))}
{suggestionsLoading && query && renderMessage(t('auto-complete.loading'))}
{suggestionsError && renderMessage(t('error.unspecific'))}
{children}
</SuggestionContainer>
);
}
};
// this format avoid arrow function eslint rule
function renderSuggestionsContainer({ containerProps, children, query }): JSX.Element {
return (
<Wrapper>
<Autosuggest
{...autosuggestProps}
inputProps={inputProps}
onSuggestionSelected={onClick}
renderSuggestionsContainer={renderSuggestionsContainer}
/>
</Wrapper>
<SuggestionContainer {...containerProps} square={true}>
{suggestionsLoaded && children === null && query && renderMessage(SUGGESTIONS_RESPONSE.NO_RESULT)}
{suggestionsLoading && query && renderMessage(SUGGESTIONS_RESPONSE.LOADING)}
{suggestionsError && renderMessage(SUGGESTIONS_RESPONSE.FAILURE)}
{children}
</SuggestionContainer>
);
}
);
return (
<Wrapper>
<Autosuggest
{...autosuggestProps}
inputProps={inputProps}
onSuggestionSelected={onClick}
renderSuggestionsContainer={renderSuggestionsContainer}
/>
</Wrapper>
);
};
export default AutoComplete;

View File

@@ -0,0 +1,37 @@
import React, { FC } from 'react';
import { isEmail } from '../../utils/url';
import Tooltip from '../../muiComponents/Tooltip';
import Avatar from '../../muiComponents/Avatar';
export interface AvatarDeveloper {
name: string;
packageName: string;
version: string;
avatar: string;
email: string;
}
const AvatarTooltip: FC<AvatarDeveloper> = ({ name, packageName, version, avatar, email }) => {
const avatarComponent = <Avatar aria-label={name} src={avatar} />;
function renderLinkForMail(
email: string,
avatarComponent: JSX.Element,
packageName: string,
version: string
): JSX.Element {
if (!email || isEmail(email) === false) {
return avatarComponent;
}
return (
<a href={`mailto:${email}?subject=${packageName}@${version}`} target={'_top'}>
{avatarComponent}
</a>
);
}
return <Tooltip title={name}>{renderLinkForMail(email, avatarComponent, packageName, version)}</Tooltip>;
};
export { AvatarTooltip };

View File

@@ -0,0 +1,4 @@
import { AvatarTooltip } from './AvatarTooltip';
export { AvatarTooltip };
export default AvatarTooltip;

View File

@@ -1,8 +1,8 @@
import FileCopy from '@material-ui/icons/FileCopy';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { copyToClipBoardUtility } from '../../utils/cli-utils';
import { TEXT } from '../../utils/constants';
import Tooltip from '../../muiComponents/Tooltip';
import { ClipBoardCopy, ClipBoardCopyText, CopyIcon } from './styles';
@@ -20,16 +20,19 @@ const renderText = (text: string, children: React.ReactNode): JSX.Element => {
return <ClipBoardCopyText>{text}</ClipBoardCopyText>;
};
const renderToolTipFileCopy = (text: string): React.ReactElement<HTMLElement> => (
<Tooltip disableFocusListener={true} title={TEXT.CLIPBOARD_COPY}>
<CopyIcon onClick={copyToClipBoardUtility(text)}>
<FileCopy />
</CopyIcon>
</Tooltip>
);
const CopyToClipBoard: React.FC<Props> = ({ text, children }) => {
const { t } = useTranslation();
return (
<ClipBoardCopy>
{renderText(text, children)}
<Tooltip disableFocusListener={true} title={t('copy-to-clipboard')}>
<CopyIcon onClick={copyToClipBoardUtility(text)}>
<FileCopy />
</CopyIcon>
</Tooltip>
{renderToolTipFileCopy(text)}
</ClipBoardCopy>
);
};

View File

@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<CopyToClipBoard /> component should load the component in default state 1`] = `"<div class=\\"css-1in239f-ClipBoardCopy eb8w2fo0\\"><span class=\\"css-7gar9h-ClipBoardCopyText eb8w2fo1\\">copy text</span><button class=\\"MuiButtonBase-root MuiIconButton-root css-1fs86cq-CopyIcon eb8w2fo2\\" tabindex=\\"0\\" type=\\"button\\" title=\\"copy-to-clipboard\\"><span class=\\"MuiIconButton-label\\"><svg class=\\"MuiSvgIcon-root\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z\\"></path></svg></span><span class=\\"MuiTouchRipple-root\\"></span></button></div>"`;
exports[`<CopyToClipBoard /> component should load the component in default state 1`] = `"<div class=\\"css-1in239f-ClipBoardCopy eb8w2fo0\\"><span class=\\"css-7gar9h-ClipBoardCopyText eb8w2fo1\\">copy text</span><button class=\\"MuiButtonBase-root MuiIconButton-root css-1fs86cq-CopyIcon eb8w2fo2\\" tabindex=\\"0\\" type=\\"button\\" title=\\"Copy to Clipboard\\"><span class=\\"MuiIconButton-label\\"><svg class=\\"MuiSvgIcon-root\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z\\"></path></svg></span><span class=\\"MuiTouchRipple-root\\"></span></button></div>"`;

View File

@@ -1,6 +1,5 @@
import React, { useContext } from 'react';
import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import CardContent from '../../muiComponents/CardContent';
import { PackageDependencies } from '../../../types/packageMeta';
@@ -17,7 +16,6 @@ interface DependencyBlockProps {
const DependencyBlock: React.FC<DependencyBlockProps> = ({ title, dependencies }) => {
const { enableLoading } = useContext(DetailContext);
const history = useHistory();
const { t } = useTranslation();
const deps = Object.entries(dependencies);
@@ -33,14 +31,8 @@ const DependencyBlock: React.FC<DependencyBlockProps> = ({ title, dependencies }
<StyledText variant="subtitle1">{`${title} (${deps.length})`}</StyledText>
<Tags>
{deps.map(([name, version]) => (
<Tag
className={'dep-tag'}
clickable={true}
key={name}
label={t('dependencies.dependency-block', { package: name, version })}
// eslint-disable-next-line
onClick={() => handleClick(name)}
/>
// eslint-disable-next-line
<Tag className={'dep-tag'} clickable={true} key={name} label={`${name}@${version}`} onClick={() => handleClick(name)} />
))}
</Tags>
</CardContent>
@@ -54,10 +46,9 @@ function hasKeys(object?: { [key: string]: any }): boolean {
const Dependencies: React.FC<{}> = () => {
const { packageMeta } = useContext(DetailContext);
const { t } = useTranslation();
if (!packageMeta) {
throw new Error(t('error.package-meta-is-required-at-detail-context'));
throw new Error('packageMeta is required at DetailContext');
}
const { latest } = packageMeta;
@@ -81,7 +72,7 @@ const Dependencies: React.FC<{}> = () => {
);
}
return <NoItems className="no-dependencies" text={t('dependencies.has-no-dependencies', { package: name })} />;
return <NoItems className="no-dependencies" text={`${name} has no dependencies.`} />;
};
export default Dependencies;

View File

@@ -1,18 +1,18 @@
import styled from '@emotion/styled';
import { fontWeight } from '../../utils/styles/sizes';
import Text from '../../muiComponents/Text';
import Card from '../../muiComponents/Card';
import Chip from '../../muiComponents/Chip';
import { Theme } from '../../design-tokens/theme';
export const CardWrap = styled(Card)({
margin: '0 0 16px',
});
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
textTransform: 'capitalize',
}));
});
export const Tags = styled('div')({
display: 'flex',

View File

@@ -1,4 +1,4 @@
import React, { useState, useContext } from 'react';
import React, { useCallback, useState, ChangeEvent, useContext } from 'react';
import { DetailContext } from '../../pages/Version';
import Box from '../../muiComponents/Box';
@@ -8,19 +8,24 @@ import DetailContainerContent from './DetailContainerContent';
import { TabPosition } from './tabs';
const DetailContainer: React.FC = () => {
const tabs = Object.values(TabPosition);
const [tabPosition, setTabPosition] = useState(0);
const [tabPosition, setTabPosition] = useState(TabPosition.README);
const detailContext = useContext(DetailContext);
const { readMe } = detailContext;
const handleChange = (event, newValue) => {
setTabPosition(newValue);
};
const handleChangeTabPosition = useCallback(
(event: ChangeEvent<{}>) => {
event.preventDefault();
const eventTarget = event.target as HTMLSpanElement;
const chosentab = eventTarget.innerText as TabPosition;
setTabPosition(TabPosition[chosentab]);
},
[setTabPosition]
);
return (
<Box component="div" display="flex" flexDirection="column" padding={2}>
<DetailContainerTabs onChange={handleChange} tabPosition={tabPosition} />
<DetailContainerContent readDescription={readMe} tabPosition={tabs[tabPosition]} />
<DetailContainerTabs onChangeTabPosition={handleChangeTabPosition} tabPosition={tabPosition} />
<DetailContainerContent readDescription={readMe} tabPosition={tabPosition} />
</Box>
);
};

View File

@@ -1,31 +1,44 @@
import React from 'react';
import React, { ChangeEvent, useState, useEffect } from 'react';
import styled from '@emotion/styled';
import { useTranslation } from 'react-i18next';
import { default as MuiTabs } from '../../muiComponents/Tabs';
import Tab from '../../muiComponents/Tab';
import { Theme } from '../../design-tokens/theme';
import { TabPosition } from './tabs';
interface Props {
onChange: (event, newValue) => void;
tabPosition: number;
tabPosition: TabPosition;
onChangeTabPosition: (event: ChangeEvent<{}>) => void;
}
const DetailContainerTabs: React.FC<Props> = ({ tabPosition, onChange }) => {
const { t } = useTranslation();
const Tabs = styled(MuiTabs)({
marginBottom: 16,
});
const getTabIndex = (tabPosition: TabPosition): number =>
Object.keys(TabPosition).findIndex(position => position === String(tabPosition).toUpperCase());
const DetailContainerTabs: React.FC<Props> = ({ tabPosition, onChangeTabPosition }) => {
const [tabPositionIndex, setTabPositionIndex] = useState(0);
useEffect(() => {
const tabIndex = getTabIndex(tabPosition);
setTabPositionIndex(tabIndex);
}, [tabPosition]);
return (
<Tabs color={'primary'} indicatorColor={'primary'} onChange={onChange} value={tabPosition} variant={'fullWidth'}>
<Tab data-testid={'readme-tab'} id={'readme-tab'} label={t('tab.readme')} />
<Tab data-testid={'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')} />
<Tabs
indicatorColor={'primary'}
onChange={onChangeTabPosition}
textColor={'primary'}
value={tabPositionIndex}
variant={'fullWidth'}>
<Tab data-testid={'readme-tab'} id={'readme-tab'} label={TabPosition.README} />
<Tab data-testid={'dependencies-tab'} id={'dependencies-tab'} label={TabPosition.DEPENDENCIES} />
<Tab data-testid={'versions-tab'} id={'versions-tab'} label={TabPosition.VERSIONS} />
<Tab data-testid={'uplinks-tab'} id={'uplinks-tab'} label={TabPosition.UPLINKS} />
</Tabs>
);
};
export default DetailContainerTabs;
const Tabs = styled(MuiTabs)<{ theme?: Theme }>({
marginBottom: 16,
});

View File

@@ -10,7 +10,6 @@ exports[`DetailContainer renders correctly 1`] = `
>
<div
class="MuiTabs-root emotion-0 emotion-1"
color="primary"
>
<div
class="MuiTabs-scroller MuiTabs-fixed"
@@ -22,7 +21,7 @@ exports[`DetailContainer renders correctly 1`] = `
>
<button
aria-selected="true"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit Mui-selected MuiTab-fullWidth"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary Mui-selected MuiTab-fullWidth"
data-testid="readme-tab"
id="readme-tab"
role="tab"
@@ -40,7 +39,7 @@ exports[`DetailContainer renders correctly 1`] = `
</button>
<button
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit MuiTab-fullWidth"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary MuiTab-fullWidth"
data-testid="dependencies-tab"
id="dependencies-tab"
role="tab"
@@ -58,7 +57,7 @@ exports[`DetailContainer renders correctly 1`] = `
</button>
<button
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit MuiTab-fullWidth"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary MuiTab-fullWidth"
data-testid="versions-tab"
id="versions-tab"
role="tab"
@@ -76,7 +75,7 @@ exports[`DetailContainer renders correctly 1`] = `
</button>
<button
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit MuiTab-fullWidth"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary MuiTab-fullWidth"
data-testid="uplinks-tab"
id="uplinks-tab"
role="tab"

View File

@@ -1,6 +1,6 @@
export enum TabPosition {
README = 'readme',
DEPENDENCIES = 'dependencies',
VERSIONS = 'versions',
UPLINKS = 'uplinks',
README = 'Readme',
DEPENDENCIES = 'Dependencies',
VERSIONS = 'Versions',
UPLINKS = 'Uplinks',
}

View File

@@ -1,52 +1,80 @@
import React, { useContext } from 'react';
import styled from '@emotion/styled';
import React, { ReactElement } from 'react';
import { DetailContext } from '../../pages/Version';
import Paper from '../../muiComponents/Paper';
import ActionBar from '../ActionBar';
import Repository from '../Repository';
import Engines from '../Engines';
import Dist from '../Dist';
import Install from '../Install';
import { ActionBar } from '../ActionBar/ActionBar';
import Author from '../Author';
import Developers, { DeveloperType } from '../Developers';
import { Theme } from '../../design-tokens/theme';
import Developers from '../Developers';
import Dist from '../Dist/Dist';
import Engine from '../Engines/Engines';
import Install from '../Install';
import Repository from '../Repository/Repository';
import { DetailContext } from '../../pages/Version';
import List from '../../muiComponents/List';
import Card from '../../muiComponents/Card';
import CardContent from '../../muiComponents/CardContent';
import DetailSidebarTitle from './DetailSidebarTitle';
import DetailSidebarFundButton from './DetailSidebarFundButton';
const DetailSidebar: React.FC = () => {
const detailContext = useContext(DetailContext);
const { packageMeta, packageName, packageVersion } = detailContext;
if (!packageMeta || !packageName) {
return null;
}
import { TitleListItem, TitleListItemText, PackageDescription, PackageVersion } from './styles';
const renderLatestDescription = (description, version, isLatest = true): JSX.Element => {
return (
<StyledPaper className={'sidebar-info'}>
<DetailSidebarTitle
description={packageMeta.latest?.description}
isLatest={typeof packageVersion === 'undefined'}
packageName={packageName}
version={packageVersion || packageMeta.latest.version}
/>
<ActionBar />
<Install />
<DetailSidebarFundButton />
<Repository />
<Engines />
<Dist />
<Author />
<Developers type={DeveloperType.MAINTAINERS} />
<Developers type={DeveloperType.CONTRIBUTORS} />
</StyledPaper>
<>
<PackageDescription>{description}</PackageDescription>
{version ? (
<PackageVersion>
<small>{`${isLatest ? 'Latest v' : 'v'}${version}`}</small>
</PackageVersion>
) : null}
</>
);
};
export default DetailSidebar;
const renderCopyCLI = (): JSX.Element => <Install />;
const renderMaintainers = (): JSX.Element => <Developers type="maintainers" />;
const renderContributors = (): JSX.Element => <Developers type="contributors" />;
const renderRepository = (): JSX.Element => <Repository />;
const renderAuthor = (): JSX.Element => <Author />;
const renderEngine = (): JSX.Element => <Engine />;
const renderDist = (): JSX.Element => <Dist />;
const renderActionBar = (): JSX.Element => <ActionBar />;
const renderTitle = (packageName, packageVersion, packageMeta): JSX.Element => {
const version = packageVersion ? packageVersion : packageMeta.latest.version;
const isLatest = typeof packageVersion === 'undefined';
const StyledPaper = styled(Paper)<{ theme?: Theme }>(({ theme }) => ({
padding: theme?.spacing(3, 2),
}));
return (
<List className="detail-info">
<TitleListItem alignItems="flex-start" button={true}>
<TitleListItemText
primary={<b>{packageName}</b>}
secondary={renderLatestDescription(packageMeta.latest.description, version, isLatest)}
/>
</TitleListItem>
</List>
);
};
function renderSideBar(packageName, packageVersion, packageMeta): ReactElement<HTMLElement> {
return (
<div className={'sidebar-info'}>
<Card>
<CardContent>
{renderTitle(packageName, packageVersion, packageMeta)}
{renderActionBar()}
{renderCopyCLI()}
{renderRepository()}
{renderEngine()}
{renderDist()}
{renderAuthor()}
{renderMaintainers()}
{renderContributors()}
</CardContent>
</Card>
</div>
);
}
const DetailSidebar = (): JSX.Element => {
const { packageName, packageMeta, packageVersion } = React.useContext(DetailContext);
return renderSideBar(packageName, packageVersion, packageMeta);
};
export default DetailSidebar;

View File

@@ -1,103 +0,0 @@
import React from 'react';
import _ from 'lodash';
import { render } from '../../utils/test-react-testing-library';
import { DetailContext, DetailContextProps } from '../../pages/Version';
import DetailSidebarFundButton from './DetailSidebarFundButton';
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({ contextValue }) => (
<DetailContext.Provider value={contextValue}>
<DetailSidebarFundButton />
</DetailContext.Provider>
);
const detailContextValue: DetailContextProps = {
packageName: 'foo',
readMe: 'test',
enableLoading: () => {},
isLoading: false,
hasNotBeenFound: false,
packageMeta: {
_uplinks: {},
latest: {
name: '@verdaccio/local-storage',
version: '8.0.1-next.1',
dist: { fileCount: 0, unpackedSize: 0, tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz' },
homepage: 'https://verdaccio.org',
bugs: {
url: 'https://github.com/verdaccio/monorepo/issues',
},
},
},
};
describe('test DetailSidebarFundButton', () => {
test('should not display the button if fund is missing', () => {
const wrapper = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(wrapper.queryByText('Fund')).toBeNull();
});
test('should not display the button if url is missing', () => {
const value = _.merge(detailContextValue, {
packageMeta: {
latest: {
funding: {},
},
},
});
const wrapper = render(<ComponentToBeRendered contextValue={value} />);
expect(wrapper.queryByText('Fund')).toBeNull();
});
test('should not display the button if url is not a string', () => {
const value = _.merge(detailContextValue, {
packageMeta: {
latest: {
funding: {
url: null,
},
},
},
});
const wrapper = render(<ComponentToBeRendered contextValue={value} />);
expect(wrapper.queryByText('Fund')).toBeNull();
});
test('should not display the button if url is not an url', () => {
const value = _.merge(detailContextValue, {
packageMeta: {
latest: {
funding: {
url: 'somethign different as url',
},
},
},
});
const wrapper = render(<ComponentToBeRendered contextValue={value} />);
expect(wrapper.queryByText('Fund')).toBeNull();
});
test('should display the button if url is a valid url', () => {
const value = _.merge(detailContextValue, {
packageMeta: {
latest: {
funding: {
url: 'https://opencollective.com/verdaccio',
},
},
},
});
const wrapper = render(<ComponentToBeRendered contextValue={value} />);
expect(wrapper.getByText('Fund')).toBeTruthy();
});
});

View File

@@ -1,48 +0,0 @@
import React, { useContext } from 'react';
import styled from '@emotion/styled';
import Favorite from '@material-ui/icons/Favorite';
import { Trans } from 'react-i18next';
import Button from '../../muiComponents/Button';
import Link from '../Link';
import { isURL } from '../../utils/url';
import { Theme } from '../../design-tokens/theme';
import { DetailContext } from '../../pages/Version';
const StyledLink = styled(Link)<{ theme?: Theme }>(({ theme }) => ({
marginTop: theme && theme.spacing(1),
marginBottom: theme && theme.spacing(1),
textDecoration: 'none',
display: 'block',
}));
const StyledFavoriteIcon = styled(Favorite)<{ theme?: Theme }>(({ theme }) => ({
color: theme && theme.palette.orange,
}));
const StyledFundStrong = styled('strong')({
marginRight: 3,
});
/* eslint-disable react/jsx-no-bind */
const DetailSidebarFundButton: React.FC = () => {
const detailContext = useContext(DetailContext);
const { packageMeta } = detailContext;
const fundingUrl = packageMeta?.latest?.funding?.url as string;
if (!isURL(fundingUrl)) {
return null;
}
return (
<StyledLink external={true} to={fundingUrl}>
<Button color="primary" fullWidth={true} startIcon={<StyledFavoriteIcon />} variant="outlined">
<Trans components={[<StyledFundStrong key="fund" />]} i18nKey="button.fund-this-package" />
</Button>
</StyledLink>
);
};
export default DetailSidebarFundButton;

View File

@@ -1,38 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
import { useTranslation } from 'react-i18next';
import Box from '../../muiComponents/Box';
import Heading from '../../muiComponents/Heading';
import { Theme } from '../../design-tokens/theme';
interface Props {
packageName: string;
description?: string;
version: string;
isLatest: boolean;
}
const DetailSidebarTitle: React.FC<Props> = ({ description, packageName, version, isLatest }) => {
const { t } = useTranslation();
return (
<Box className={'detail-info'} display="flex" flexDirection="column" marginBottom="8px">
<StyledHeading>{packageName}</StyledHeading>
{description && <div>{description}</div>}
<StyledBoxVersion>
{isLatest ? t('sidebar.detail.latest-version', { version }) : t('sidebar.detail.version', { version })}
</StyledBoxVersion>
</Box>
);
};
export default DetailSidebarTitle;
const StyledHeading = styled(Heading)({
fontSize: '1rem',
fontWeight: 700,
});
const StyledBoxVersion = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
color: theme && theme.palette.text.secondary,
}));

View File

@@ -0,0 +1,24 @@
import styled from '@emotion/styled';
import ListItem from '../../muiComponents/ListItem';
import ListItemText from '../../muiComponents/ListItemText';
export const TitleListItem = styled(ListItem)({
paddingLeft: 0,
paddingRight: 0,
paddingBottom: 0,
});
export const TitleListItemText = styled(ListItemText)({
paddingLeft: 0,
paddingRight: 0,
paddingTop: '8px',
});
export const PackageDescription = styled('span')({
display: 'block',
});
export const PackageVersion = styled('span')({
display: 'block',
});

View File

@@ -3,8 +3,8 @@ import React from 'react';
import { mount } from '../../utils/test-enzyme';
import { DetailContextProvider } from '../../pages/Version';
import Developers, { Fab } from './Developers';
import { DeveloperType } from './types';
import Developers, { DevelopersType } from './Developers';
import { Fab } from './styles';
describe('test Developers', () => {
const packageMeta = {
@@ -35,13 +35,14 @@ describe('test Developers', () => {
};
test('should render the component with no items', () => {
const type: DevelopersType = 'maintainers';
const packageMeta = {
latest: {},
};
const wrapper = mount(
// @ts-ignore
<DetailContextProvider value={{ packageMeta }}>
<Developers type={DeveloperType.MAINTAINERS} />
<Developers type={type} />
</DetailContextProvider>
);
@@ -49,10 +50,11 @@ describe('test Developers', () => {
});
test('should render the component for maintainers with items', () => {
const type: DevelopersType = 'maintainers';
const wrapper = mount(
// @ts-ignore
<DetailContextProvider value={{ packageMeta }}>
<Developers type={DeveloperType.MAINTAINERS} />
<Developers type={type} />
</DetailContextProvider>
);
@@ -60,10 +62,11 @@ describe('test Developers', () => {
});
test('should render the component for contributors with items', () => {
const type: DevelopersType = 'contributors';
const wrapper = mount(
// @ts-ignore
<DetailContextProvider value={{ packageMeta }}>
<Developers type={DeveloperType.CONTRIBUTORS} />
<Developers type={type} />
</DetailContextProvider>
);
@@ -71,6 +74,7 @@ describe('test Developers', () => {
});
test('should test onClick the component avatar', () => {
const type: DevelopersType = 'contributors';
const packageMeta = {
latest: {
packageName: 'foo',
@@ -91,7 +95,7 @@ describe('test Developers', () => {
const wrapper = mount(
// @ts-ignore
<DetailContextProvider value={{ packageMeta }}>
<Developers type={DeveloperType.CONTRIBUTORS} visibleMax={1} />
<Developers type={type} visibleMax={1} />
</DetailContextProvider>
);

View File

@@ -1,81 +1,60 @@
import React, { useState, useCallback, useContext, useEffect, useMemo } from 'react';
import React, { FC, Fragment } from 'react';
import Add from '@material-ui/icons/Add';
import styled from '@emotion/styled';
import { useTranslation } from 'react-i18next';
import { DetailContext } from '../../pages/Version';
import Tooltip from '../../muiComponents/Tooltip';
import Avatar from '../../muiComponents/Avatar';
import Box from '../../muiComponents/Box';
import FloatingActionButton from '../../muiComponents/FloatingActionButton';
import { Theme } from '../../design-tokens/theme';
import { AvatarTooltip } from '../AvatarTooltip';
import getUniqueDeveloperValues from './get-unique-developer-values';
import DevelopersTitle from './DevelopersTitle';
import { DeveloperType } from './types';
import { Details, StyledText, Content, Fab } from './styles';
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>(props => ({
backgroundColor: props.theme && props.theme.palette.primary.main,
color: props.theme && props.theme.palette.white,
}));
export type DevelopersType = 'contributors' | 'maintainers';
interface Props {
type: DeveloperType;
type: DevelopersType;
visibleMax?: number;
}
const StyledBox = styled(Box)({
'> *': {
margin: 5,
},
});
export const VISIBLE_MAX = 6;
const Developers: React.FC<Props> = ({ type, visibleMax = VISIBLE_MAX }) => {
const detailContext = useContext(DetailContext);
const { t } = useTranslation();
const Developers: FC<Props> = ({ type, visibleMax }) => {
const [visibleDevs, setVisibleDevs] = React.useState<number>(visibleMax || VISIBLE_MAX);
const { packageMeta } = React.useContext(DetailContext);
if (!detailContext) {
throw Error(t('app-context-not-correct-used'));
const handleLoadMore = (): void => {
setVisibleDevs(visibleDevs + VISIBLE_MAX);
};
const renderDeveloperDetails = ({ name, avatar, email }, packageMeta): JSX.Element => {
const { name: packageName, version } = packageMeta.latest;
return <AvatarTooltip avatar={avatar} email={email} name={name} packageName={packageName} version={version} />;
};
const renderDevelopers = (developers, packageMeta): JSX.Element => {
const listVisibleDevelopers = developers.slice(0, visibleDevs);
return (
<Fragment>
<StyledText variant={'subtitle1'}>{type}</StyledText>
<Content>
{listVisibleDevelopers.map(developer => (
<Details key={developer.email}>{renderDeveloperDetails(developer, packageMeta)}</Details>
))}
{visibleDevs < developers.length && (
<Fab onClick={handleLoadMore} size="small">
<Add />
</Fab>
)}
</Content>
</Fragment>
);
};
const developerList = packageMeta && packageMeta.latest[type];
if (!developerList || developerList.length === 0) {
return null;
}
const developers = useMemo(() => getUniqueDeveloperValues(detailContext.packageMeta?.latest[type]), [
detailContext.packageMeta,
type,
]);
const [visibleDevelopersMax, setVisibleDevelopersMax] = useState(visibleMax);
const [visibleDevelopers, setVisibleDevelopers] = useState(developers);
useEffect(() => {
if (!developers) return;
setVisibleDevelopers(developers.slice(0, visibleDevelopersMax));
}, [developers, visibleDevelopersMax]);
const handleSetVisibleDevelopersMax = useCallback(() => {
setVisibleDevelopersMax(visibleDevelopersMax + VISIBLE_MAX);
}, [visibleDevelopersMax]);
if (!visibleDevelopers || !developers) return null;
return (
<>
<DevelopersTitle type={type} />
<StyledBox display="flex" flexWrap="wrap" margin="10px 0 10px 0">
{visibleDevelopers.map(visibleDeveloper => (
<Tooltip key={visibleDeveloper.email} title={visibleDeveloper.name}>
<Avatar alt={visibleDeveloper.name} src={visibleDeveloper.avatar} />
</Tooltip>
))}
{visibleDevelopersMax < developers.length && (
<Fab onClick={handleSetVisibleDevelopersMax} size="small">
<Add />
</Fab>
)}
</StyledBox>
</>
);
return renderDevelopers(developerList, packageMeta);
};
export default Developers;

View File

@@ -1,30 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
import { useTranslation } from 'react-i18next';
import Text from '../../muiComponents/Text';
import { Theme } from '../../design-tokens/theme';
import { DeveloperType } from './types';
interface Props {
type: DeveloperType;
}
const DevelopersTitle: React.FC<Props> = ({ type }) => {
const { t } = useTranslation();
switch (type) {
case DeveloperType.CONTRIBUTORS:
return <StyledText variant={'subtitle1'}>{t('sidebar.contributors.title')}</StyledText>;
case DeveloperType.MAINTAINERS:
return <StyledText variant={'subtitle1'}>{t('sidebar.maintainers.title')}</StyledText>;
return null;
}
};
export default DevelopersTitle;
const StyledText = styled(Text)<{ theme?: Theme }>(({ theme }) => ({
fontWeight: theme && theme.fontWeight.bold,
marginBottom: '10px',
}));

View File

@@ -1,12 +0,0 @@
import { Developer } from '../../../types/packageMeta';
function getUniqueDeveloperValues(developers?: Array<Developer>): undefined | Array<Developer> {
if (!developers) return;
return developers.reduce(
(accumulator: Array<Developer>, current: Developer) =>
accumulator.some(developer => developer.email === current.email) ? accumulator : [...accumulator, current],
[]
);
}
export default getUniqueDeveloperValues;

View File

@@ -1,2 +1 @@
export { default } from './Developers';
export { DeveloperType } from './types';

View File

@@ -1,5 +1,6 @@
import styled from '@emotion/styled';
import { fontWeight } from '../../utils/styles/sizes';
import Text from '../../muiComponents/Text';
import FloatingActionButton from '../../muiComponents/FloatingActionButton';
import { Theme } from '../../design-tokens/theme';
@@ -19,11 +20,11 @@ export const Content = styled('div')({
},
});
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
marginBottom: '10px',
textTransform: 'capitalize',
}));
});
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>(props => ({
backgroundColor: props.theme && props.theme.palette.primary.main,

View File

@@ -1,4 +0,0 @@
export enum DeveloperType {
CONTRIBUTORS = 'contributors',
MAINTAINERS = 'maintainers',
}

View File

@@ -1,5 +1,4 @@
import React, { FC, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { DetailContext } from '../../pages/Version';
import fileSizeSI from '../../utils/file-size';
@@ -11,6 +10,8 @@ import { StyledText, DistListItem, DistChips } from './styles';
const DistChip: FC<{ name: string }> = ({ name, children }) =>
children ? (
<DistChips
// lint rule conflicting with prettier
/* eslint-disable react/jsx-wrap-multilines */
label={
<>
<b>{name}</b>
@@ -18,12 +19,12 @@ const DistChip: FC<{ name: string }> = ({ name, children }) =>
{children}
</>
}
/* eslint-enable */
/>
) : null;
const Dist: FC = () => {
const { packageMeta } = useContext(DetailContext);
const { t } = useTranslation();
if (!packageMeta) {
return null;
@@ -32,11 +33,11 @@ const Dist: FC = () => {
const { dist, license } = packageMeta && packageMeta.latest;
return (
<List subheader={<StyledText variant="subtitle1">{t('sidebar.distribution.title')}</StyledText>}>
<List subheader={<StyledText variant="subtitle1">{'Latest Distribution'}</StyledText>}>
<DistListItem button={true}>
<DistChip name={t('sidebar.distribution.file-count')}>{dist.fileCount}</DistChip>
<DistChip name={t('sidebar.distribution.size')}>{dist.unpackedSize && fileSizeSI(dist.unpackedSize)}</DistChip>
<DistChip name={t('sidebar.distribution.license')}>{formatLicense(license)}</DistChip>
<DistChip name="file count">{dist.fileCount}</DistChip>
<DistChip name="size">{dist.unpackedSize && fileSizeSI(dist.unpackedSize)}</DistChip>
<DistChip name="license">{formatLicense(license)}</DistChip>
</DistListItem>
</List>
);

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Dist /> component should render the component in default state 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText estxrtg0 MuiTypography-subtitle1\\">sidebar.distribution.title</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1mms18p-DistListItem estxrtg1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>sidebar.distribution.file-count</b>: 7</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>sidebar.distribution.size</b>: 10.00 Bytes</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<Dist /> component should render the component in default state 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText estxrtg0 MuiTypography-subtitle1\\">Latest Distribution</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1mms18p-DistListItem estxrtg1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>file count</b>: 7</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>size</b>: 10.00 Bytes</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<Dist /> component should render the component with license as object 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText estxrtg0 MuiTypography-subtitle1\\">sidebar.distribution.title</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1mms18p-DistListItem estxrtg1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>sidebar.distribution.file-count</b>: 7</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>sidebar.distribution.size</b>: 10.00 Bytes</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>sidebar.distribution.license</b>: MIT</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<Dist /> component should render the component with license as object 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText estxrtg0 MuiTypography-subtitle1\\">Latest Distribution</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1mms18p-DistListItem estxrtg1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>file count</b>: 7</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>size</b>: 10.00 Bytes</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>license</b>: MIT</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<Dist /> component should render the component with license as string 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText estxrtg0 MuiTypography-subtitle1\\">sidebar.distribution.title</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1mms18p-DistListItem estxrtg1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>sidebar.distribution.file-count</b>: 7</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>sidebar.distribution.size</b>: 10.00 Bytes</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>sidebar.distribution.license</b>: MIT</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;
exports[`<Dist /> component should render the component with license as string 1`] = `"<ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText estxrtg0 MuiTypography-subtitle1\\">Latest Distribution</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-1mms18p-DistListItem estxrtg1 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>file count</b>: 7</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>size</b>: 10.00 Bytes</span></div><div class=\\"MuiChip-root css-e2le7v-DistChips estxrtg2\\"><span class=\\"MuiChip-label\\"><b>license</b>: MIT</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`;

View File

@@ -1,15 +1,16 @@
import styled from '@emotion/styled';
import { Theme } from '../../design-tokens/theme';
import { fontWeight } from '../../utils/styles/sizes';
import ListItem from '../../muiComponents/ListItem';
import Text from '../../muiComponents/Text';
import FloatingActionButton from '../../muiComponents/FloatingActionButton';
import Chip from '../../muiComponents/Chip';
import { Theme } from '../../design-tokens/theme';
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
textTransform: 'capitalize',
}));
});
export const DistListItem = styled(ListItem)({
paddingLeft: 0,

View File

@@ -1,5 +1,4 @@
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { DetailContext } from '../../pages/Version';
import Avatar from '../../muiComponents/Avatar';
@@ -13,19 +12,19 @@ import node from './img/node.png';
const Engine: React.FC = () => {
const { packageMeta } = useContext(DetailContext);
const { t } = useTranslation();
const engines = packageMeta?.latest?.engines;
const engines = packageMeta && packageMeta.latest && packageMeta.latest.engines;
if (!engines || (!engines.node && !engines.npm)) {
return null;
}
/* eslint-disable react/jsx-max-depth */
return (
<Grid container={true}>
{engines.node && (
<Grid item={true} xs={6}>
<List subheader={<StyledText variant={'subtitle1'}>{t('sidebar.engines.node-js')}</StyledText>}>
<List subheader={<StyledText variant={'subtitle1'}>{'node JS'}</StyledText>}>
<EngineListItem button={true}>
<Avatar src={node} />
<ListItemText primary={engines.node} />
@@ -36,7 +35,7 @@ const Engine: React.FC = () => {
{engines.npm && (
<Grid item={true} xs={6}>
<List subheader={<StyledText variant={'subtitle1'}>{t('sidebar.engines.npm-version')}</StyledText>}>
<List subheader={<StyledText variant={'subtitle1'}>{'NPM version'}</StyledText>}>
<EngineListItem button={true}>
<Avatar src={npm} />
<ListItemText primary={engines.npm} />
@@ -46,6 +45,7 @@ const Engine: React.FC = () => {
)}
</Grid>
);
/* eslint-enable react/jsx-max-depth */
};
export default Engine;

View File

@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Engines /> component should render the component in default state 1`] = `"<div class=\\"MuiGrid-root MuiGrid-container\\"><div class=\\"MuiGrid-root MuiGrid-item MuiGrid-grid-xs-6\\"><ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText et66bt70 MuiTypography-subtitle1\\">sidebar.engines.node-js</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-18b06t0-EngineListItem et66bt71 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault\\"><svg class=\\"MuiSvgIcon-root MuiAvatar-fallback\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><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\\"></path></svg></div><div class=\\"MuiListItemText-root\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">&gt;= 0.1.98</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul></div><div class=\\"MuiGrid-root MuiGrid-item MuiGrid-grid-xs-6\\"><ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText et66bt70 MuiTypography-subtitle1\\">sidebar.engines.npm-version</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-18b06t0-EngineListItem et66bt71 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault\\"><svg class=\\"MuiSvgIcon-root MuiAvatar-fallback\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><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\\"></path></svg></div><div class=\\"MuiListItemText-root\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">&gt;3</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul></div></div>"`;
exports[`<Engines /> component should render the component in default state 1`] = `"<div class=\\"MuiGrid-root MuiGrid-container\\"><div class=\\"MuiGrid-root MuiGrid-item MuiGrid-grid-xs-6\\"><ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText et66bt70 MuiTypography-subtitle1\\">node JS</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-18b06t0-EngineListItem et66bt71 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault\\"></div><div class=\\"MuiListItemText-root\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">&gt;= 0.1.98</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul></div><div class=\\"MuiGrid-root MuiGrid-item MuiGrid-grid-xs-6\\"><ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText et66bt70 MuiTypography-subtitle1\\">NPM version</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-18b06t0-EngineListItem et66bt71 MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault\\"></div><div class=\\"MuiListItemText-root\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">&gt;3</span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul></div></div>"`;

View File

@@ -1,13 +1,13 @@
import styled from '@emotion/styled';
import { fontWeight } from '../../utils/styles/sizes';
import ListItem from '../../muiComponents/ListItem';
import Text from '../../muiComponents/Text';
import { Theme } from '../../design-tokens/theme';
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
textTransform: 'capitalize',
}));
});
export const EngineListItem = styled(ListItem)({
paddingLeft: 0,

View File

@@ -1,38 +1,53 @@
import React from 'react';
import { useTranslation, Trans } from 'react-i18next';
import { goToVerdaccioWebsite } from '../../utils/windows';
import { Wrapper, Left, Right, Earth, Flags, Love, Flag, Logo, Inner, ToolTip } from './styles';
/* eslint-disable react/jsx-key */
const Footer: React.FC = () => {
const { t } = useTranslation();
const renderTooltip = (): JSX.Element => (
<ToolTip>
<Earth name="earth" size="md" />
<Flags>
<Flag name="spain" size="md" />
<Flag name="nicaragua" size="md" />
<Flag name="india" size="md" />
<Flag name="brazil" size="md" />
<Flag name="china" size="md" />
<Flag name="austria" size="md" />
</Flags>
</ToolTip>
);
const POWERED_LABEL = 'Powered by';
const MADEWITH_LABEL = ' Made with';
const ON_LABEL = 'on';
const HEARTH_EMOJI = '♥';
const renderRight = (version = window.VERDACCIO_VERSION): JSX.Element => {
return (
<Wrapper>
<Inner>
<Left>
<Trans components={[<Love />]} i18nKey="footer.made-with-love-on" />
<ToolTip>
<Earth name="earth" size="md" />
<Flags>
<Flag name="spain" size="md" />
<Flag name="nicaragua" size="md" />
<Flag name="india" size="md" />
<Flag name="brazil" size="md" />
<Flag name="china" size="md" />
<Flag name="austria" size="md" />
</Flags>
</ToolTip>
</Left>
<Right>
{t('footer.powered-by')}
<Logo img={true} name="verdaccio" onClick={goToVerdaccioWebsite} pointer={true} size="md" />
{`/ ${window.VERDACCIO_VERSION}`}
</Right>
</Inner>
</Wrapper>
<Right>
{POWERED_LABEL}
<Logo img={true} name="verdaccio" onClick={goToVerdaccioWebsite} pointer={true} size="md" />
{`/ ${version}`}
</Right>
);
};
const renderLeft = (): JSX.Element => (
<Left>
{MADEWITH_LABEL}
<Love>{HEARTH_EMOJI}</Love>
{ON_LABEL}
{renderTooltip()}
</Left>
);
const Footer: React.FC = () => (
<Wrapper>
<Inner>
{renderLeft()}
{renderRight()}
</Inner>
</Wrapper>
);
export default Footer;

View File

@@ -167,13 +167,13 @@ exports[`<Footer /> component should load the initial state of Footer component
<div
class="emotion-27 emotion-28"
>
Made with
Made with
<span
class="emotion-0 emotion-1"
>
♥
</span>
on
on
<span
class="emotion-25 emotion-26"
>

View File

@@ -1,39 +1,40 @@
import styled from '@emotion/styled';
import { breakpoints } from '../../utils/styles/media';
import Icon from '../Icon/Icon';
import { Theme } from '../../design-tokens/theme';
export const Wrapper = styled('div')<{ theme?: Theme }>(({ theme }) => ({
background: theme?.palette.type === 'dark' ? theme?.palette.primary.main : theme?.palette.snow,
borderTop: `1px solid ${theme?.palette.greyGainsboro}`,
color: theme?.palette.type === 'dark' ? theme?.palette.white : theme?.palette.nobel01,
export const Wrapper = styled('div')<{ theme?: Theme }>(props => ({
background: props.theme && props.theme.palette.snow,
borderTop: `1px solid ${props.theme && props.theme.palette.greyGainsboro}`,
color: props.theme && props.theme.palette.nobel01,
fontSize: '14px',
padding: '20px',
}));
export const Inner = styled('div')<{ theme?: Theme }>(({ theme }) => ({
export const Inner = styled('div')({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
width: '100%',
[`@media (min-width: ${theme && theme.breakPoints.medium}px)`]: {
[`@media (min-width: ${breakpoints.medium}px)`]: {
minWidth: 400,
maxWidth: 800,
margin: 'auto',
justifyContent: 'space-between',
},
[`@media (min-width: ${theme && theme.breakPoints.large}px)`]: {
[`@media (min-width: ${breakpoints.large}px)`]: {
maxWidth: 1240,
},
}));
});
export const Left = styled('div')<{ theme?: Theme }>(({ theme }) => ({
export const Left = styled('div')({
alignItems: 'center',
display: 'none',
[`@media (min-width: ${theme && theme.breakPoints.medium}px)`]: {
[`@media (min-width: ${breakpoints.medium}px)`]: {
display: 'flex',
},
}));
});
export const Right = styled(Left)({
display: 'flex',

View File

@@ -1,17 +1,16 @@
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { render, fireEvent, waitForElement, waitForElementToBeRemoved } from '../../utils/test-react-testing-library';
import { AppContextProvider } from '../../App';
import translationEN from '../../../i18n/translations/en-US.json';
import { render, fireEvent, waitForElementToBeRemoved, waitForElement } from '../../utils/test-react-testing-library';
import Header from './Header';
const props = {
user: {
username: 'verddacio-user',
},
packages: [],
const headerProps = {
username: 'verddacio-user',
scope: 'test scope',
withoutSearch: true,
handleToggleLoginModal: jest.fn(),
handleLogout: jest.fn(),
};
/* eslint-disable react/jsx-no-bind*/
@@ -19,70 +18,82 @@ describe('<Header /> component with logged in state', () => {
test('should load the component in logged out state', () => {
const { container, queryByTestId, getByText } = render(
<Router>
<AppContextProvider>
<Header />
</AppContextProvider>
<Header
onLogout={headerProps.handleLogout}
onToggleLoginModal={headerProps.handleToggleLoginModal}
scope={headerProps.scope}
/>
</Router>
);
expect(container.firstChild).toMatchSnapshot();
expect(queryByTestId('header--menu-accountcircle')).toBeNull();
expect(queryByTestId('header--menu-acountcircle')).toBeNull();
expect(getByText('Login')).toBeTruthy();
});
test('should load the component in logged in state', () => {
const { container, getByTestId, queryByText } = render(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
<Header
onLogout={headerProps.handleLogout}
onToggleLoginModal={headerProps.handleToggleLoginModal}
scope={headerProps.scope}
username={headerProps.username}
/>
</Router>
);
expect(container.firstChild).toMatchSnapshot();
expect(getByTestId('header--menu-accountcircle')).toBeTruthy();
expect(getByTestId('header--menu-acountcircle')).toBeTruthy();
expect(queryByText('Login')).toBeNull();
});
test('should open login dialog', async () => {
const { getByTestId } = render(
const { getByText } = render(
<Router>
<AppContextProvider>
<Header />
</AppContextProvider>
<Header
onLogout={headerProps.handleLogout}
onToggleLoginModal={headerProps.handleToggleLoginModal}
scope={headerProps.scope}
/>
</Router>
);
const loginBtn = getByTestId('header--button-login');
const loginBtn = getByText('Login');
fireEvent.click(loginBtn);
const loginDialog = await waitForElement(() => getByTestId('login--dialog'));
expect(loginDialog).toBeTruthy();
expect(headerProps.handleToggleLoginModal).toHaveBeenCalled();
});
test('should logout the user', async () => {
const { getByText, getByTestId } = render(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
<Header
onLogout={headerProps.handleLogout}
onToggleLoginModal={headerProps.handleToggleLoginModal}
scope={headerProps.scope}
username={headerProps.username}
/>
</Router>
);
const headerMenuAccountCircle = getByTestId('header--menu-accountcircle');
const headerMenuAccountCircle = getByTestId('header--menu-acountcircle');
fireEvent.click(headerMenuAccountCircle);
// wait for button Logout's appearance and return the element
const logoutBtn = await waitForElement(() => getByText('Logout'));
fireEvent.click(logoutBtn);
expect(getByText('Login')).toBeTruthy();
expect(headerProps.handleLogout).toHaveBeenCalled();
});
test("The question icon should open a new tab of verdaccio's website - installation doc", () => {
test("The question icon should open a new tab of verdaccio's website - installation doc", async () => {
const { getByTestId } = render(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
<Header
onLogout={headerProps.handleLogout}
onToggleLoginModal={headerProps.handleToggleLoginModal}
scope={headerProps.scope}
username={headerProps.username}
/>
</Router>
);
@@ -93,9 +104,12 @@ describe('<Header /> component with logged in state', () => {
test('should open the registrationInfo modal when clicking on the info icon', async () => {
const { getByTestId } = render(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
<Header
onLogout={headerProps.handleLogout}
onToggleLoginModal={headerProps.handleToggleLoginModal}
scope={headerProps.scope}
username={headerProps.username}
/>
</Router>
);
@@ -110,9 +124,12 @@ describe('<Header /> component with logged in state', () => {
test('should close the registrationInfo modal when clicking on the button close', async () => {
const { getByTestId, getByText, queryByTestId } = render(
<Router>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
<Header
onLogout={headerProps.handleLogout}
onToggleLoginModal={headerProps.handleToggleLoginModal}
scope={headerProps.scope}
username={headerProps.username}
/>
</Router>
);
@@ -120,13 +137,13 @@ 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 waitForElement(() => getByText(translationEN.button.close));
const closeBtn = await waitForElement(() => getByText('CLOSE'));
fireEvent.click(closeBtn);
const hasRegistrationInfoModalBeenRemoved = await waitForElementToBeRemoved(() =>
queryByTestId('registryInfo--dialog')
);
expect(hasRegistrationInfoModalBeenRemoved).toBeTruthy();
test.todo('autocompletion should display suggestions according to the type value');
});
test.todo('autocompletion should display suggestions according to the type value');
});

View File

@@ -1,12 +1,8 @@
import React, { useState, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import React, { useState } from 'react';
import storage from '../../utils/storage';
import Search from '../Search';
import { getRegistryURL } from '../../utils/url';
import Button from '../../muiComponents/Button';
import AppContext from '../../App/AppContext';
import LoginDialog from '../LoginDialog';
import Search from '../Search';
import { NavBar, InnerNavBar, MobileNavBar, InnerMobileNavBar } from './styles';
import HeaderLeft from './HeaderLeft';
@@ -14,44 +10,31 @@ import HeaderRight from './HeaderRight';
import HeaderInfoDialog from './HeaderInfoDialog';
interface Props {
logo?: string;
username?: string;
onLogout: () => void;
onToggleLoginModal: () => void;
scope: string;
withoutSearch?: boolean;
}
/* eslint-disable react/jsx-max-depth */
/* eslint-disable react/jsx-no-bind*/
const Header: React.FC<Props> = ({ withoutSearch }) => {
const { t } = useTranslation();
const appContext = useContext(AppContext);
const Header: React.FC<Props> = ({ logo, withoutSearch, username, onLogout, onToggleLoginModal, scope }) => {
const [isInfoDialogOpen, setOpenInfoDialog] = useState();
const [showMobileNavBar, setShowMobileNavBar] = useState();
const [showLoginModal, setShowLoginModal] = useState(false);
if (!appContext) {
throw Error(t('app-context-not-correct-used'));
}
const { user, scope, setUser } = appContext;
/**
* Logouts user
* Required by: <Header />
*/
const handleLogout = () => {
storage.removeItem('username');
storage.removeItem('token');
setUser(undefined);
};
return (
<>
<NavBar data-testid="header" position="static">
<NavBar position="static">
<InnerNavBar>
<HeaderLeft />
<HeaderLeft logo={logo} />
<HeaderRight
onLogout={handleLogout}
onLogout={onLogout}
onOpenRegistryInfoDialog={() => setOpenInfoDialog(true)}
onToggleLogin={() => setShowLoginModal(!showLoginModal)}
onToggleLogin={onToggleLoginModal}
onToggleMobileNav={() => setShowMobileNavBar(!showMobileNavBar)}
username={user && user.username}
username={username}
withoutSearch={withoutSearch}
/>
</InnerNavBar>
@@ -68,11 +51,10 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
<Search />
</InnerMobileNavBar>
<Button color="inherit" onClick={() => setShowMobileNavBar(false)}>
{t('button.cancel')}
{'Cancel'}
</Button>
</MobileNavBar>
)}
{!user && <LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />}
</>
);
};

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Label from '../Label';
@@ -9,14 +8,11 @@ interface Props {
username: string;
}
const HeaderGreetings: React.FC<Props> = ({ username }) => {
const { t } = useTranslation();
return (
<>
<Greetings>{t('header.greetings')}</Greetings>
<Label capitalize={true} data-testid="greetings-label" text={username} weight="bold" />
</>
);
};
const HeaderGreetings: React.FC<Props> = ({ username }) => (
<>
<Greetings>{'Hi,'}</Greetings>
<Label capitalize={true} text={username} weight="bold" />
</>
);
export default HeaderGreetings;

View File

@@ -3,22 +3,23 @@ import styled from '@emotion/styled';
import { Link } from 'react-router-dom';
import Search from '../Search/';
import Logo from '../Logo';
import HeaderLogo from './HeaderLogo';
import { LeftSide, SearchWrapper } from './styles';
interface Props {
withoutSearch?: boolean;
logo?: string;
}
const StyledLink = styled(Link)({
marginRight: '1em',
});
const HeaderLeft: React.FC<Props> = ({ withoutSearch = false }) => (
const HeaderLeft: React.FC<Props> = ({ withoutSearch = false, logo }) => (
<LeftSide>
<StyledLink to={'/'}>
<Logo />
<HeaderLogo logo={logo} />
</StyledLink>
{!withoutSearch && (
<SearchWrapper>

View File

@@ -0,0 +1,17 @@
import React from 'react';
import Logo from '../Logo';
interface Props {
logo?: string;
}
const HeaderLogo: React.FC<Props> = ({ logo }) => {
if (logo) {
return <img alt="logo" height="40px" src={logo} />;
}
return <Logo />;
};
export default HeaderLogo;

View File

@@ -1,5 +1,4 @@
import React, { MouseEvent } from 'react';
import { useTranslation } from 'react-i18next';
import AccountCircle from '@material-ui/icons/AccountCircle';
import IconButton from '../../muiComponents/IconButton';
@@ -17,6 +16,7 @@ interface Props {
onLoggedInMenuClose: () => void;
}
/* eslint-disable react/jsx-max-depth */
const HeaderMenu: React.FC<Props> = ({
onLogout,
username,
@@ -24,38 +24,35 @@ const HeaderMenu: React.FC<Props> = ({
anchorEl,
onLoggedInMenu,
onLoggedInMenuClose,
}) => {
const { t } = useTranslation();
return (
<>
<IconButton
color="inherit"
data-testid="header--menu-accountcircle"
id="header--button-account"
onClick={onLoggedInMenu}>
<AccountCircle />
</IconButton>
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
onClose={onLoggedInMenuClose}
open={isMenuOpen}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}>
<MenuItem disabled={true}>
<HeaderGreetings username={username} />
</MenuItem>
<MenuItem button={true} data-testid="header--button-logout" id="header--button-logout" onClick={onLogout}>
{t('button.logout')}
</MenuItem>
</Menu>
</>
);
};
}) => (
<>
<IconButton
color="inherit"
data-testid="header--menu-acountcircle"
id="header--button-account"
onClick={onLoggedInMenu}>
<AccountCircle />
</IconButton>
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
onClose={onLoggedInMenuClose}
open={isMenuOpen}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}>
<MenuItem disabled={true}>
<HeaderGreetings username={username} />
</MenuItem>
<MenuItem button={true} id="header--button-logout" onClick={onLogout}>
{'Logout'}
</MenuItem>
</Menu>
</>
);
export default HeaderMenu;

View File

@@ -1,8 +1,6 @@
import React, { useState, useEffect, useContext, MouseEvent } from 'react';
import { useTranslation } from 'react-i18next';
import React, { useState, useEffect, MouseEvent } from 'react';
import Button from '../../muiComponents/Button';
import ThemeContext from '../../design-tokens/ThemeContext';
import { RightSide } from './styles';
import HeaderToolTip from './HeaderToolTip';
@@ -25,16 +23,9 @@ const HeaderRight: React.FC<Props> = ({
onToggleMobileNav,
onOpenRegistryInfoDialog,
}) => {
const themeContext = useContext(ThemeContext);
const [anchorEl, setAnchorEl] = useState();
const [isMenuOpen, setIsMenuOpen] = useState();
const { t } = useTranslation();
if (!themeContext) {
throw Error(t('theme-context-not-correct-used'));
}
useEffect(() => {
setIsMenuOpen(Boolean(anchorEl));
}, [anchorEl]);
@@ -61,25 +52,13 @@ const HeaderRight: React.FC<Props> = ({
onToggleLogin();
};
const handleToggleDarkLightMode = () => {
setTimeout(() => {
themeContext.setIsDarkMode(!themeContext.isDarkMode);
}, 300);
};
return (
<RightSide data-testid="header-right">
<RightSide>
{!withoutSearch && (
<HeaderToolTip onClick={onToggleMobileNav} title={t('search.packages')} tooltipIconType={'search'} />
<HeaderToolTip onClick={onToggleMobileNav} title={'Search packages'} tooltipIconType={'search'} />
)}
<HeaderToolTip title={t('header.documentation')} tooltipIconType={'help'} />
<HeaderToolTip onClick={onOpenRegistryInfoDialog} title={t('header.registry-info')} tooltipIconType={'info'} />
<HeaderToolTip
onClick={handleToggleDarkLightMode}
title={t('header.documentation')}
tooltipIconType={themeContext.isDarkMode ? 'dark-mode' : 'light-mode'}
/>
<HeaderToolTip title={'Documentation'} tooltipIconType={'help'} />
<HeaderToolTip onClick={onOpenRegistryInfoDialog} title={'Registry Information'} tooltipIconType={'info'} />
{username ? (
<HeaderMenu
anchorEl={anchorEl}
@@ -91,7 +70,7 @@ const HeaderRight: React.FC<Props> = ({
/>
) : (
<Button color="inherit" data-testid="header--button-login" onClick={handleToggleLogin}>
{t('button.login')}
{'Login'}
</Button>
)}
</RightSide>

View File

@@ -2,14 +2,12 @@ import React, { forwardRef } from 'react';
import Info from '@material-ui/icons/Info';
import Help from '@material-ui/icons/Help';
import Search from '@material-ui/icons/Search';
import NightsStay from '@material-ui/icons/NightsStay';
import WbSunny from '@material-ui/icons/WbSunny';
import IconButton from '../../muiComponents/IconButton';
import { IconSearchButton, StyledLink } from './styles';
export type TooltipIconType = 'search' | 'help' | 'info' | 'dark-mode' | 'light-mode';
export type TooltipIconType = 'search' | 'help' | 'info';
interface Props {
tooltipIconType: TooltipIconType;
onClick?: () => void;
@@ -52,21 +50,6 @@ const HeaderToolTipIcon = forwardRef<HeaderToolTipIconRef, Props>(function Heade
<Search />
</IconSearchButton>
);
case 'dark-mode':
// todo(Priscila): Add Zoom transition effect
return (
<IconButton color="inherit" onClick={onClick} ref={ref}>
<NightsStay />
</IconButton>
);
case 'light-mode':
// todo(Priscila): Add Zoom transition effect
return (
<IconButton color="inherit" onClick={onClick} ref={ref}>
<WbSunny />
</IconButton>
);
default:
return null;
}

View File

@@ -141,7 +141,6 @@ exports[`<Header /> component with logged in state should load the component in
<header
class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-24 emotion-25 MuiAppBar-colorPrimary"
data-testid="header"
>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-22 emotion-23 MuiToolbar-gutters"
@@ -214,7 +213,6 @@ exports[`<Header /> component with logged in state should load the component in
</div>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-20 emotion-21 MuiToolbar-gutters"
data-testid="header-right"
>
<button
class="MuiButtonBase-root MuiIconButton-root emotion-16 emotion-17 MuiIconButton-colorInherit"
@@ -304,31 +302,7 @@ exports[`<Header /> component with logged in state should load the component in
</button>
<button
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit"
tabindex="0"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<button
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit"
data-testid="header--menu-accountcircle"
data-testid="header--menu-acountcircle"
id="header--button-account"
tabindex="0"
type="button"
@@ -498,7 +472,6 @@ exports[`<Header /> component with logged in state should load the component in
<header
class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-24 emotion-25 MuiAppBar-colorPrimary"
data-testid="header"
>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-22 emotion-23 MuiToolbar-gutters"
@@ -571,7 +544,6 @@ exports[`<Header /> component with logged in state should load the component in
</div>
<div
class="MuiToolbar-root MuiToolbar-regular emotion-20 emotion-21 MuiToolbar-gutters"
data-testid="header-right"
>
<button
class="MuiButtonBase-root MuiIconButton-root emotion-16 emotion-17 MuiIconButton-colorInherit"
@@ -659,30 +631,6 @@ exports[`<Header /> component with logged in state should load the component in
class="MuiTouchRipple-root"
/>
</button>
<button
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit"
tabindex="0"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit"
data-testid="header--button-login"

View File

@@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { css } from '@emotion/core';
import { Theme } from '../../design-tokens/theme';
import { breakpoints } from '../../utils/styles/media';
import IconButton from '../../muiComponents/IconButton';
import AppBar from '../../muiComponents/AppBar';
import Toolbar from '../../muiComponents/Toolbar';
@@ -53,12 +54,12 @@ export const SearchWrapper = styled('div')({
width: '100%',
});
export const NavBar = styled(AppBar)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor: theme && theme.palette.primary.main,
export const NavBar = styled(AppBar)<{ theme?: Theme }>(props => ({
backgroundColor: props.theme && props.theme.palette.primary.main,
minHeight: 60,
display: 'flex',
justifyContent: 'center',
[`@media (min-width: ${theme && theme.breakPoints.medium}px)`]: css`
[`@media (min-width: ${breakpoints.medium}px)`]: css`
${SearchWrapper} {
display: flex;
}
@@ -69,12 +70,12 @@ export const NavBar = styled(AppBar)<{ theme?: Theme }>(({ theme }) => ({
display: none;
}
`,
[`@media (min-width: ${theme && theme.breakPoints.large}px)`]: css`
[`@media (min-width: ${breakpoints.large}px)`]: css`
${InnerNavBar} {
padding: 0 20px;
}
`,
[`@media (min-width: ${theme && theme.breakPoints.xlarge}px)`]: css`
[`@media (min-width: ${breakpoints.xlarge}px)`]: css`
${InnerNavBar} {
max-width: 1240px;
width: 100%;

View File

@@ -1,5 +1,4 @@
import React, { Fragment } from 'react';
import { useTranslation } from 'react-i18next';
import { getRegistryURL } from '../../utils/url';
import CopyToClipBoard from '../CopyToClipBoard';
@@ -25,24 +24,23 @@ function renderHeadingClipboardSegments(title: string, text: string): React.Reac
const Help: React.FC = () => {
const registryUrl = getRegistryURL();
const { t } = useTranslation();
return (
<Card id="help-card">
<CardContent>
<Typography component="h2" gutterBottom={true} id={COMPONENT_HELP_ID} variant="h5">
{t('help.title')}
{HELP_TITLE}
</Typography>
<HelpTitle color="textSecondary" gutterBottom={true}>
{t('help.sub-title')}
{'To publish your first package just:'}
</HelpTitle>
{renderHeadingClipboardSegments(t('help.first-step'), t('help.first-step-command-line', { registryUrl }))}
{renderHeadingClipboardSegments(t('help.second-step'), t('help.second-step-command-line', { registryUrl }))}
<Text variant="body2">{t('help.third-step')}</Text>
{renderHeadingClipboardSegments('1. Login', `npm adduser --registry ${registryUrl}`)}
{renderHeadingClipboardSegments('2. Publish', `npm publish --registry ${registryUrl}`)}
<Text variant="body2">{'3. Refresh this page.'}</Text>
</CardContent>
<CardActions>
<Button color="primary" href="https://verdaccio.org/docs/en/installation" size="small">
{t('button.learn-more')}
{'Learn More'}
</Button>
</CardActions>
</Card>

View File

@@ -68,7 +68,7 @@ exports[`<Help /> component should load the component in default state 1`] = `
<button
class="MuiButtonBase-root MuiIconButton-root emotion-4 emotion-5"
tabindex="0"
title="Copy to clipboard"
title="Copy to Clipboard"
type="button"
>
<span
@@ -107,7 +107,7 @@ exports[`<Help /> component should load the component in default state 1`] = `
<button
class="MuiButtonBase-root MuiIconButton-root emotion-4 emotion-5"
tabindex="0"
title="Copy to clipboard"
title="Copy to Clipboard"
type="button"
>
<span

View File

@@ -1,21 +1,19 @@
import React, { useContext } from 'react';
import styled from '@emotion/styled';
import { useTranslation } from 'react-i18next';
import { DetailContext } from '../../pages/Version';
import { fontWeight } from '../../utils/styles/sizes';
import Text from '../../muiComponents/Text';
import List from '../../muiComponents/List';
import { Theme } from '../../design-tokens/theme';
import InstallListItem, { DependencyManager } from './InstallListItem';
const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
textTransform: 'capitalize',
}));
});
const Install: React.FC = () => {
const { t } = useTranslation();
const detailContext = useContext(DetailContext);
const { packageMeta, packageName } = detailContext;
@@ -25,9 +23,7 @@ const Install: React.FC = () => {
}
return (
<List
data-testid={'installList'}
subheader={<StyledText variant={'subtitle1'}>{t('sidebar.installation.title')}</StyledText>}>
<List data-testid={'installList'} subheader={<StyledText variant={'subtitle1'}>{'Installation'}</StyledText>}>
<InstallListItem dependencyManager={DependencyManager.NPM} packageName={packageName} />
<InstallListItem dependencyManager={DependencyManager.YARN} packageName={packageName} />
<InstallListItem dependencyManager={DependencyManager.PNPM} packageName={packageName} />

View File

@@ -1,6 +1,5 @@
import React from 'react';
import styled from '@emotion/styled';
import { useTranslation } from 'react-i18next';
import CopyToClipBoard from '../CopyToClipBoard';
import Avatar from '../../muiComponents/Avatar';
@@ -27,9 +26,6 @@ const InstallListItemText = styled(ListItemText)({
const PackageMangerAvatar = styled(Avatar)({
borderRadius: '0px',
padding: '0',
img: {
backgroundColor: 'transparent',
},
});
export enum DependencyManager {
@@ -44,16 +40,14 @@ interface Interface {
}
const InstallListItem: React.FC<Interface> = ({ packageName, dependencyManager }) => {
const { t } = useTranslation();
switch (dependencyManager) {
case DependencyManager.NPM:
return (
<InstallItem button={true} data-testid={'installListItem-npm'}>
<PackageMangerAvatar alt="npm" src={npmLogo} />
<InstallListItemText
primary={<CopyToClipBoard text={t('sidebar.installation.install-using-npm-command', { packageName })} />}
secondary={t('sidebar.installation.install-using-npm')}
primary={<CopyToClipBoard text={`npm install ${packageName}`} />}
secondary={'Install using npm'}
/>
</InstallItem>
);
@@ -62,8 +56,8 @@ const InstallListItem: React.FC<Interface> = ({ packageName, dependencyManager }
<InstallItem button={true} data-testid={'installListItem-yarn'}>
<PackageMangerAvatar alt="yarn" src={yarnLogo} />
<InstallListItemText
primary={<CopyToClipBoard text={t('sidebar.installation.install-using-yarn-command', { packageName })} />}
secondary={t('sidebar.installation.install-using-yarn')}
primary={<CopyToClipBoard text={`yarn add ${packageName}`} />}
secondary={'Install using yarn'}
/>
</InstallItem>
);
@@ -72,8 +66,8 @@ const InstallListItem: React.FC<Interface> = ({ packageName, dependencyManager }
<InstallItem button={true} data-testid={'installListItem-pnpm'}>
<PackageMangerAvatar alt={'pnpm'} src={pnpmLogo} />
<InstallListItemText
primary={<CopyToClipBoard text={t('sidebar.installation.install-using-pnpm-command', { packageName })} />}
secondary={t('sidebar.installation.install-using-pnpm')}
primary={<CopyToClipBoard text={`pnpm install ${packageName}`} />}
secondary={'Install using pnpm'}
/>
</InstallItem>
);

View File

@@ -19,10 +19,6 @@ exports[`<Install /> renders correctly 1`] = `
padding: 0;
}
.emotion-2 img {
background-color: transparent;
}
.emotion-10 {
padding: 0 10px;
margin: 0;
@@ -94,7 +90,7 @@ exports[`<Install /> renders correctly 1`] = `
<button
class="MuiButtonBase-root MuiIconButton-root emotion-6 emotion-7"
tabindex="0"
title="Copy to clipboard"
title="Copy to Clipboard"
type="button"
>
<span
@@ -161,7 +157,7 @@ exports[`<Install /> renders correctly 1`] = `
<button
class="MuiButtonBase-root MuiIconButton-root emotion-6 emotion-7"
tabindex="0"
title="Copy to clipboard"
title="Copy to Clipboard"
type="button"
>
<span
@@ -228,7 +224,7 @@ exports[`<Install /> renders correctly 1`] = `
<button
class="MuiButtonBase-root MuiIconButton-root emotion-6 emotion-7"
tabindex="0"
title="Copy to clipboard"
title="Copy to Clipboard"
type="button"
>
<span

View File

@@ -1,7 +1,7 @@
import React from 'react';
import styled from '@emotion/styled';
import { Theme } from '../../design-tokens/theme';
import { fontWeight } from '../../utils/styles/sizes';
interface Props {
text: string;
@@ -14,8 +14,8 @@ interface WrapperProps {
weight: string;
}
const Wrapper = styled('div')<WrapperProps & { theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight[props.weight],
const Wrapper = styled('div')<WrapperProps>(props => ({
fontWeight: fontWeight[props.weight],
textTransform: props.capitalize ? 'capitalize' : 'none',
}));

View File

@@ -1,4 +1,4 @@
import React, { MouseEvent } from 'react';
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import Text, { TextProps } from '../../muiComponents/Text';
@@ -7,27 +7,20 @@ interface Props extends Pick<TextProps, 'variant'> {
external?: boolean;
className?: string;
to: string;
children?: React.ReactNode;
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
}
type LinkRef = HTMLAnchorElement;
/* eslint-disable verdaccio/jsx-spread */
const Link = React.forwardRef<LinkRef, Props>(function Link(
{ external, to, children, variant, className, ...props },
ref
) {
const Link: React.FC<Props> = ({ external, to, children, variant, className, ...props }) => {
const LinkTextContent = <Text variant={variant}>{children}</Text>;
return external ? (
<a className={className} href={to} ref={ref} rel="noopener noreferrer" target="_blank" {...props}>
<a className={className} href={to} rel="noopener noreferrer" target="_blank" {...props}>
{LinkTextContent}
</a>
) : (
<RouterLink className={className} innerRef={ref} to={to} {...props}>
<RouterLink className={className} to={to} {...props}>
{LinkTextContent}
</RouterLink>
);
});
};
export default Link;

View File

@@ -1,7 +1,5 @@
import styled from '@emotion/styled';
import { Theme } from '../../design-tokens/theme';
export const Wrapper = styled('div')({
transform: 'translate(-50%, -50%)',
top: '50%',
@@ -9,9 +7,9 @@ export const Wrapper = styled('div')({
position: 'absolute',
});
export const Badge = styled('div')<{ theme?: Theme }>(({ theme }) => ({
export const Badge = styled('div')({
margin: '0 0 30px 0',
borderRadius: 25,
boxShadow: '0 10px 20px 0 rgba(69, 58, 100, 0.2)',
background: theme?.palette.type === 'dark' ? theme?.palette.black : '#f7f8f6',
}));
background: '#f7f8f6',
});

View File

@@ -0,0 +1,127 @@
/**
* @prettier
*/
import React from 'react';
import { mount } from '../../utils/test-enzyme';
import LoginModal from './Login';
const eventUsername = {
target: {
value: 'xyz',
},
};
const eventPassword = {
target: {
value: '1234',
},
};
const event = {
preventDefault: jest.fn(),
};
describe('<LoginModal />', () => {
test('should load the component in default state', () => {
const wrapper = mount(<LoginModal />);
expect(wrapper.html()).toMatchSnapshot();
});
test('should load the component with props', () => {
const props = {
visibility: true,
error: {
type: 'error',
title: 'Error Title',
description: 'Error Description',
},
onCancel: () => {},
onSubmit: () => {},
};
const wrapper = mount(<LoginModal {...props} />);
expect(wrapper.html()).toMatchSnapshot();
});
test('onCancel: should close the login modal', () => {
const props = {
visibility: true,
error: {
type: 'error',
title: 'Error Title',
description: 'Error Description',
},
onCancel: jest.fn(),
onSubmit: () => {},
};
const wrapper = mount(<LoginModal {...props} />);
wrapper.find('button[id="login--form-cancel"]').simulate('click');
expect(props.onCancel).toHaveBeenCalled();
});
test('setCredentials - should set username and password in state', () => {
const props = {
visibility: true,
onCancel: () => {},
onSubmit: () => {},
};
const wrapper = mount(<LoginModal {...props} />);
const { setCredentials } = wrapper.instance();
expect(setCredentials('username', eventUsername)).toBeUndefined();
expect(wrapper.state('form').username.value).toEqual('xyz');
expect(setCredentials('password', eventPassword)).toBeUndefined();
expect(wrapper.state('form').password.value).toEqual('1234');
});
test('validateCredentials: should validate credentials', async () => {
const props = {
visibility: true,
onCancel: () => {},
onSubmit: jest.fn(),
};
const wrapper = mount(<LoginModal {...props} />);
const instance = wrapper.instance();
instance.submitCredentials = jest.fn();
const { handleValidateCredentials, setCredentials, submitCredentials } = instance;
expect(setCredentials('username', eventUsername)).toBeUndefined();
expect(wrapper.state('form').username.value).toEqual('xyz');
expect(setCredentials('password', eventPassword)).toBeUndefined();
expect(wrapper.state('form').password.value).toEqual('1234');
handleValidateCredentials(event);
expect(event.preventDefault).toHaveBeenCalled();
expect(wrapper.state('form').username.pristine).toEqual(false);
expect(wrapper.state('form').password.pristine).toEqual(false);
expect(submitCredentials).toHaveBeenCalledTimes(1);
});
test('submitCredentials: should submit credentials', async () => {
const props = {
onSubmit: jest.fn(),
};
const wrapper = mount(<LoginModal {...props} />);
const { setCredentials, submitCredentials } = wrapper.instance();
expect(setCredentials('username', eventUsername)).toBeUndefined();
expect(wrapper.state('form').username.value).toEqual('xyz');
expect(setCredentials('password', eventPassword)).toBeUndefined();
expect(wrapper.state('form').password.value).toEqual('1234');
await submitCredentials();
expect(props.onSubmit).toHaveBeenCalledWith('xyz', '1234');
expect(wrapper.state('form').username.value).toEqual('');
expect(wrapper.state('form').username.pristine).toEqual(true);
expect(wrapper.state('form').password.value).toEqual('');
expect(wrapper.state('form').password.pristine).toEqual(true);
});
});

View File

@@ -0,0 +1,248 @@
import React, { Component } from 'react';
import ErrorIcon from '@material-ui/icons/Error';
import Button from '../../muiComponents/Button';
import Dialog from '../../muiComponents/Dialog';
import DialogTitle from '../../muiComponents/DialogTitle';
import DialogContent from '../../muiComponents/DialogContent';
import DialogActions from '../../muiComponents/DialogActions';
import FormControl from '../../muiComponents/FormControl';
import FormHelperText from '../../muiComponents/FormHelperText';
import Input from '../../muiComponents/Input';
import InputLabel from '../../muiComponents/InputLabel';
import SnackbarContent from '../../muiComponents/SnackbarContent';
import * as classes from './styles';
interface FormFields {
required: boolean;
pristine: boolean;
helperText: string;
value: string;
}
export interface FormError {
type: string;
title: string;
description: string;
}
interface LoginModalProps {
visibility: boolean;
error?: FormError;
onCancel: () => void;
onSubmit: (username: string, password: string) => void;
}
interface LoginModalState {
form: {
username: Partial<FormFields>;
password: Partial<FormFields>;
};
error?: FormError;
}
export default class LoginModal extends Component<Partial<LoginModalProps>, LoginModalState> {
constructor(props: LoginModalProps) {
super(props);
this.state = {
form: {
username: {
required: true,
pristine: true,
helperText: 'Field required',
value: '',
},
password: {
required: true,
pristine: true,
helperText: 'Field required',
value: '',
},
},
error: props.error,
};
}
public render(): JSX.Element {
const { visibility = true, onCancel = () => null, error } = this.props as LoginModalProps;
return (
<Dialog fullWidth={true} id={'login--form-container'} maxWidth={'xs'} onClose={onCancel} open={visibility}>
<form noValidate={true} onSubmit={this.handleValidateCredentials}>
<DialogTitle>{'Login'}</DialogTitle>
<DialogContent>
{error && this.renderLoginError(error)}
{this.renderNameField()}
{this.renderPasswordField()}
</DialogContent>
{this.renderActions()}
</form>
</Dialog>
);
}
/**
* set login modal's username and password to current state
* Required to login
*/
public setCredentials = (name, e) => {
const { form } = this.state;
this.setState({
form: {
...form,
[name]: {
...form[name],
value: e.target.value,
pristine: false,
},
},
});
};
public handleUsernameChange = event => {
this.setCredentials('username', event);
};
public handlePasswordChange = event => {
this.setCredentials('password', event);
};
public handleValidateCredentials = event => {
const { form } = this.state;
// prevents default submit behavior
event.preventDefault();
this.setState(
{
form: Object.keys(form).reduce(
(acc, key) => ({
...acc,
...{ [key]: { ...form[key], pristine: false } },
}),
{ username: {}, password: {} }
),
},
() => {
if (!Object.keys(form).some(id => !form[id])) {
this.submitCredentials();
}
}
);
};
public submitCredentials = async () => {
const { form } = this.state;
const username = (form.username && form.username.value) || '';
const password = (form.password && form.password.value) || '';
const { onSubmit } = this.props;
if (onSubmit) {
await onSubmit(username, password);
}
// let's wait for API response and then set
// username and password filed to empty state
this.setState({
form: Object.keys(form).reduce(
(acc, key) => ({
...acc,
...{ [key]: { ...form[key], value: '', pristine: true } },
}),
{ username: {}, password: {} }
),
});
};
public renderErrorMessage(title: string, description: string): JSX.Element {
return (
<span>
<div>
<strong>{title}</strong>
</div>
<div>{description}</div>
</span>
);
}
public renderMessage(title: string, description: string): JSX.Element {
return (
<div className={classes.loginErrorMsg} id={'client-snackbar'}>
<ErrorIcon className={classes.loginIcon} />
{this.renderErrorMessage(title, description)}
</div>
);
}
public renderLoginError({ type, title, description }: FormError): JSX.Element | false {
return (
type === 'error' && (
<SnackbarContent className={classes.loginError} message={this.renderMessage(title, description)} />
)
);
}
public renderNameField = () => {
const {
form: { username },
} = this.state;
return (
<FormControl error={!username.value && !username.pristine} fullWidth={true} required={username.required}>
<InputLabel htmlFor={'username'}>{'Username'}</InputLabel>
<Input
id={'login--form-username'}
onChange={this.handleUsernameChange}
placeholder={'Your username'}
value={username.value}
/>
{!username.value && !username.pristine && (
<FormHelperText id={'username-error'}>{username.helperText}</FormHelperText>
)}
</FormControl>
);
};
public renderPasswordField = () => {
const {
form: { password },
} = this.state;
return (
<FormControl
// className={css`
// margin-top: 8px;
// `}
error={!password.value && !password.pristine}
fullWidth={true}
required={password.required}>
<InputLabel htmlFor={'password'}>{'Password'}</InputLabel>
<Input
id={'login--form-password'}
onChange={this.handlePasswordChange}
placeholder={'Your strong password'}
type={'password'}
value={password.value}
/>
{!password.value && !password.pristine && (
<FormHelperText id={'password-error'}>{password.helperText}</FormHelperText>
)}
</FormControl>
);
};
public renderActions = () => {
const {
form: { username, password },
} = this.state;
const { onCancel } = this.props;
return (
<DialogActions className={'dialog-footer'}>
<Button color={'inherit'} id={'login--form-cancel'} onClick={onCancel} type={'button'}>
{'Cancel'}
</Button>
<Button
color={'inherit'}
disabled={!password.value || !username.value}
id={'login--form-submit'}
type={'submit'}>
{'Login'}
</Button>
</DialogActions>
);
};
}

View File

@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LoginModal /> should load the component in default state 1`] = `"<div role=\\"presentation\\" class=\\"MuiDialog-root\\" id=\\"login--form-container\\" style=\\"position: fixed; z-index: 1300; right: 0px; bottom: 0px; top: 0px; left: 0px;\\"><div class=\\"MuiBackdrop-root\\" aria-hidden=\\"true\\" style=\\"opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\\"></div><div tabindex=\\"0\\" data-test=\\"sentinelStart\\"></div><div class=\\"MuiDialog-container MuiDialog-scrollPaper\\" style=\\"opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\\" role=\\"none presentation\\" tabindex=\\"-1\\"><div class=\\"MuiPaper-root MuiPaper-elevation24 MuiDialog-paper MuiDialog-paperScrollPaper MuiDialog-paperWidthXs MuiDialog-paperFullWidth MuiPaper-rounded\\" role=\\"dialog\\"><form novalidate=\\"\\"><div class=\\"MuiDialogTitle-root\\"><h2 class=\\"MuiTypography-root MuiTypography-h6\\">Login</h2></div><div class=\\"MuiDialogContent-root\\"><div class=\\"MuiFormControl-root MuiFormControl-fullWidth\\"><label class=\\"MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required\\" data-shrink=\\"false\\" for=\\"username\\">Username<span class=\\"MuiFormLabel-asterisk MuiInputLabel-asterisk\\"> *</span></label><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl\\"><input aria-invalid=\\"false\\" id=\\"login--form-username\\" placeholder=\\"Your username\\" required=\\"\\" type=\\"text\\" class=\\"MuiInputBase-input MuiInput-input\\" value=\\"\\"></div></div><div class=\\"MuiFormControl-root MuiFormControl-fullWidth\\"><label class=\\"MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required\\" data-shrink=\\"false\\" for=\\"password\\">Password<span class=\\"MuiFormLabel-asterisk MuiInputLabel-asterisk\\"> *</span></label><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl\\"><input aria-invalid=\\"false\\" id=\\"login--form-password\\" placeholder=\\"Your strong password\\" required=\\"\\" type=\\"password\\" class=\\"MuiInputBase-input MuiInput-input\\" value=\\"\\"></div></div></div><div class=\\"MuiDialogActions-root dialog-footer MuiDialogActions-spacing\\"><button class=\\"MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit\\" tabindex=\\"0\\" type=\\"button\\" id=\\"login--form-cancel\\"><span class=\\"MuiButton-label\\">Cancel</span><span class=\\"MuiTouchRipple-root\\"></span></button><button class=\\"MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit Mui-disabled Mui-disabled\\" tabindex=\\"-1\\" type=\\"submit\\" disabled=\\"\\" id=\\"login--form-submit\\"><span class=\\"MuiButton-label\\">Login</span></button></div></form></div></div><div tabindex=\\"0\\" data-test=\\"sentinelEnd\\"></div></div>"`;
exports[`<LoginModal /> should load the component with props 1`] = `"<div role=\\"presentation\\" class=\\"MuiDialog-root\\" id=\\"login--form-container\\" style=\\"position: fixed; z-index: 1300; right: 0px; bottom: 0px; top: 0px; left: 0px;\\"><div class=\\"MuiBackdrop-root\\" aria-hidden=\\"true\\" style=\\"opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\\"></div><div tabindex=\\"0\\" data-test=\\"sentinelStart\\"></div><div class=\\"MuiDialog-container MuiDialog-scrollPaper\\" style=\\"opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\\" role=\\"none presentation\\" tabindex=\\"-1\\"><div class=\\"MuiPaper-root MuiPaper-elevation24 MuiDialog-paper MuiDialog-paperScrollPaper MuiDialog-paperWidthXs MuiDialog-paperFullWidth MuiPaper-rounded\\" role=\\"dialog\\"><form novalidate=\\"\\"><div class=\\"MuiDialogTitle-root\\"><h2 class=\\"MuiTypography-root MuiTypography-h6\\">Login</h2></div><div class=\\"MuiDialogContent-root\\"><div class=\\"MuiTypography-root MuiPaper-root MuiPaper-elevation6 MuiSnackbarContent-root css-xlgaf-loginError MuiTypography-body2\\" role=\\"alert\\"><div class=\\"MuiSnackbarContent-message\\"><div class=\\"css-vvv32-loginErrorMsg\\" id=\\"client-snackbar\\"><svg class=\\"MuiSvgIcon-root css-tkvt8h-loginIcon\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z\\"></path></svg><span><div><strong>Error Title</strong></div><div>Error Description</div></span></div></div></div><div class=\\"MuiFormControl-root MuiFormControl-fullWidth\\"><label class=\\"MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required\\" data-shrink=\\"false\\" for=\\"username\\">Username<span class=\\"MuiFormLabel-asterisk MuiInputLabel-asterisk\\"> *</span></label><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl\\"><input aria-invalid=\\"false\\" id=\\"login--form-username\\" placeholder=\\"Your username\\" required=\\"\\" type=\\"text\\" class=\\"MuiInputBase-input MuiInput-input\\" value=\\"\\"></div></div><div class=\\"MuiFormControl-root MuiFormControl-fullWidth\\"><label class=\\"MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required\\" data-shrink=\\"false\\" for=\\"password\\">Password<span class=\\"MuiFormLabel-asterisk MuiInputLabel-asterisk\\"> *</span></label><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl\\"><input aria-invalid=\\"false\\" id=\\"login--form-password\\" placeholder=\\"Your strong password\\" required=\\"\\" type=\\"password\\" class=\\"MuiInputBase-input MuiInput-input\\" value=\\"\\"></div></div></div><div class=\\"MuiDialogActions-root dialog-footer MuiDialogActions-spacing\\"><button class=\\"MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit\\" tabindex=\\"0\\" type=\\"button\\" id=\\"login--form-cancel\\"><span class=\\"MuiButton-label\\">Cancel</span><span class=\\"MuiTouchRipple-root\\"></span></button><button class=\\"MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit Mui-disabled Mui-disabled\\" tabindex=\\"-1\\" type=\\"submit\\" disabled=\\"\\" id=\\"login--form-submit\\"><span class=\\"MuiButton-label\\">Login</span></button></div></form></div></div><div tabindex=\\"0\\" data-test=\\"sentinelEnd\\"></div></div>"`;

View File

@@ -0,0 +1 @@
export { default } from './Login';

View File

@@ -0,0 +1,23 @@
import { css } from 'emotion';
import { theme } from '../../design-tokens/theme';
export const loginDialog = css({
minWidth: '300px',
});
export const loginError = css({
backgroundColor: `${theme.palette.red} !important`,
minWidth: 'inherit !important',
marginBottom: '10px !important',
});
export const loginErrorMsg = css({
display: 'flex',
alignItems: 'center',
});
export const loginIcon = css({
opacity: 0.9,
marginRight: '8px',
});

View File

@@ -1,106 +0,0 @@
import React from 'react';
import { render, waitForElement, fireEvent } from '../../utils/test-react-testing-library';
import AppContext, { AppContextProps } from '../../App/AppContext';
import api from '../../utils/api';
import translationEN from '../../../i18n/translations/en-US.json';
import LoginDialog from './LoginDialog';
const appContextValue: AppContextProps = {
scope: '',
setUser: jest.fn(),
};
describe('<LoginDialog /> component', () => {
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
});
test('should render the component in default state', () => {
const props = {
onClose: jest.fn(),
};
const { container } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} />
</AppContext.Provider>
);
expect(container.firstChild).toMatchSnapshot();
});
test('should load the component with the open prop', async () => {
const props = {
open: true,
onClose: jest.fn(),
};
const { getByTestId } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
);
const loginDialogHeading = await waitForElement(() => getByTestId('login-dialog-form-login-button'));
expect(loginDialogHeading).toBeTruthy();
});
test('onClose: should close the login modal', async () => {
const props = {
open: true,
onClose: jest.fn(),
};
const { getByTestId } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
);
const loginDialogButton = await waitForElement(() => getByTestId('close-login-dialog-button'));
expect(loginDialogButton).toBeTruthy();
fireEvent.click(loginDialogButton, { open: false });
expect(props.onClose).toHaveBeenCalled();
});
// TODO
test.skip('setCredentials - should set username and password in state', async () => {
const props = {
open: true,
onClose: jest.fn(),
};
jest.spyOn(api, 'request').mockImplementation(() =>
Promise.resolve({
username: 'xyz',
token: 'djsadaskljd',
})
);
const { getByPlaceholderText, getByText } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
);
// TODO: the input's value is not being updated in the DOM
const userNameInput = getByPlaceholderText('Your username');
fireEvent.focus(userNameInput);
fireEvent.change(userNameInput, { target: { value: 'xyz' } });
// TODO: the input's value is not being updated in the DOM
const passwordInput = getByPlaceholderText('Your strong password');
fireEvent.focus(passwordInput);
fireEvent.change(passwordInput, { target: { value: '1234' } });
// TODO: submitting form does not work
const signInButton = getByText('Sign in');
fireEvent.click(signInButton);
});
test.todo('validateCredentials: should validate credentials');
test.todo('submitCredentials: should submit credentials');
});

Some files were not shown because too many files have changed in this diff Show More