1
0
mirror of https://github.com/SomboChea/ui synced 2026-01-18 09:06:14 +07:00

Compare commits

...

56 Commits

Author SHA1 Message Date
Juan Picado @jotadeveloper
6bb37d6656 chore(release): 0.3.9 2019-12-14 15:06:07 +01:00
Juan Picado @jotadeveloper
ac1a4fa46d chore: test publish 2019-12-14 15:05:36 +01:00
Juan Picado @jotadeveloper
3b228a2a0b chore(release): 0.3.8 2019-12-14 14:34:45 +01:00
Priscila Oliveira
fd99be6818 Refactor: move styles utils to theme (#363) 2019-12-12 12:10:27 -03:00
dependabot-preview[bot]
172e470780 build(deps-dev): bump @types/node from 12.12.16 to 12.12.17 (#362) 2019-12-12 11:23:34 -03:00
dependabot-preview[bot]
a3b41747ca build(deps-dev): bump @material-ui/core from 4.7.1 to 4.7.2 (#350) 2019-12-12 11:22:38 -03:00
dependabot-preview[bot]
7e29182a15 build(deps-dev): bump concurrently from 5.0.0 to 5.0.1 (#353) 2019-12-11 10:34:57 -03:00
dependabot-preview[bot]
8e89c82750 build(deps-dev): bump css-loader from 3.2.1 to 3.3.0 (#354) 2019-12-11 10:34:23 -03:00
dependabot-preview[bot]
f1e468e7e4 build(deps-dev): bump eslint-plugin-import from 2.18.2 to 2.19.1 (#349) 2019-12-11 10:32:28 -03:00
dependabot-preview[bot]
28208d6633 build(deps-dev): bump @babel/plugin-proposal-optional-chaining (#348) 2019-12-11 10:32:17 -03:00
dependabot-preview[bot]
398c5804cf build(deps-dev): bump @types/react from 16.9.15 to 16.9.16 (#355) 2019-12-11 10:31:47 -03:00
dependabot-preview[bot]
4d9ac2bd04 build(deps-dev): bump @typescript-eslint/parser from 2.10.0 to 2.11.0 (#356) 2019-12-11 10:19:30 -03:00
dependabot-preview[bot]
52c941be09 build(deps-dev): bump react-hook-form from 3.28.12 to 3.28.15 (#352) 2019-12-11 10:06:20 -03:00
dependabot-preview[bot]
89b554b07c build(deps-dev): bump @types/enzyme from 3.10.3 to 3.10.4 (#351) 2019-12-11 09:00:52 -03:00
dependabot-preview[bot]
43a6bc0133 build(deps-dev): bump @testing-library/react from 9.3.2 to 9.3.3 (#359)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 9.3.2 to 9.3.3.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v9.3.2...v9.3.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-11 09:02:36 +01:00
dependabot-preview[bot]
e40280ffeb build(deps-dev): bump @types/node from 12.12.14 to 12.12.16 (#357) 2019-12-10 15:42:35 +01:00
Priscila Oliveira
ae617a5c04 fix: removed deade import (#346) 2019-12-07 08:45:51 +01:00
Thomas Klein
33f873a8c7 fix: formatDate (#308) 2019-12-06 18:28:37 +01:00
Priscila Oliveira
42d3bb8508 feat: login Dialog Component - Replaced class by func. comp + added react-hook-form (#341)
* refactor: convert class to func

* refactor: changed login form logic

* refactor: conver to testing-library tests

* refactor: moved dependency

* refactor: replaced uglifyjs-webpack-plugin by terser-webpack-plugin

* fix: fixed e2e errors

* fix: fixed e2e test

* Delete settings.json

* fix: vscode settings rollback

* refactor: rollback webpack config

* refactor: updated eslint rule

* fix: removed --fix

* refactor: incresed the bundle size
2019-12-06 18:09:01 +01:00
Priscila Oliveira
501845b5f8 refactor: replaced date fns with dayjs (#345) 2019-12-06 17:58:24 +01:00
dependabot-preview[bot]
474e9e18de build(deps-dev): bump stylelint-webpack-plugin from 1.1.1 to 1.1.2 (#344) 2019-12-05 15:38:06 +01:00
dependabot-preview[bot]
ab810c8caa build(deps-dev): bump typescript from 3.7.2 to 3.7.3 (#342) 2019-12-05 15:37:41 +01:00
dependabot-preview[bot]
a3d7acfd73 build(deps-dev): bump @types/react from 16.9.14 to 16.9.15 (#343) 2019-12-05 15:37:30 +01:00
Priscila Oliveira
6ba721446b Search Component - Replaced class by func. comp (#339) 2019-12-04 17:09:02 +01:00
Priscila Oliveira
09b831a40d fix: updated actionbar snap (#340) 2019-12-03 14:24:10 +01:00
Priscila Oliveira
742971db0d ActionBar Component - Replaced class by func. comp (#330) 2019-12-03 09:44:44 +01:00
dependabot-preview[bot]
fcad6fa794 build(deps-dev): bump css-loader from 3.2.0 to 3.2.1 (#337) 2019-12-03 09:42:09 +01:00
dependabot-preview[bot]
a7b5e6df99 build(deps-dev): bump @typescript-eslint/parser from 2.9.0 to 2.10.0 (#338) 2019-12-03 09:41:39 +01:00
dependabot-preview[bot]
c988f0fac7 build(deps-dev): bump jest-emotion from 10.0.25 to 10.0.26 (#335) 2019-12-03 09:41:23 +01:00
dependabot-preview[bot]
c839970a25 build(deps-dev): bump @types/react from 16.9.13 to 16.9.14 (#336) 2019-12-03 09:40:54 +01:00
dependabot-preview[bot]
200cc289e6 build(deps-dev): bump @material-ui/core from 4.6.1 to 4.7.1 (#331) 2019-12-02 16:05:25 +01:00
dependabot-preview[bot]
11d66b7df1 build(deps-dev): bump eslint from 6.7.1 to 6.7.2 (#333) 2019-12-02 15:28:06 +01:00
dependabot-preview[bot]
7c616fa81a build(deps-dev): bump stylelint-webpack-plugin from 1.1.0 to 1.1.1 (#334) 2019-12-02 15:27:26 +01:00
dependabot-preview[bot]
e6dbf0a187 build(deps-dev): bump jest-emotion from 10.0.17 to 10.0.25 (#332) 2019-12-02 15:27:05 +01:00
dependabot-preview[bot]
764e73bbe2 build(deps-dev): bump codeceptjs from 2.3.5 to 2.3.6 (#318) 2019-12-01 16:19:34 +01:00
Priscila Oliveira
e60ab9e247 Repository Component - Replaced class by func. comp (#323) 2019-12-01 16:14:17 +01:00
dependabot-preview[bot]
d37de29d36 build(deps-dev): bump file-loader from 4.3.0 to 5.0.2 (#322) 2019-12-01 16:12:02 +01:00
dependabot-preview[bot]
764090dad3 build(deps-dev): bump url-loader from 2.3.0 to 3.0.0 (#324) 2019-12-01 16:08:08 +01:00
dependabot-preview[bot]
bedcea9a83 build(deps-dev): bump eslint-plugin-react from 7.16.0 to 7.17.0 (#329) 2019-12-01 15:43:07 +01:00
dependabot-preview[bot]
76142ecda6 build(deps-dev): bump lockfile-lint from 3.0.1 to 3.0.3 (#328) 2019-12-01 15:42:43 +01:00
dependabot-preview[bot]
ddb3b15cf6 build(deps-dev): bump style-loader from 1.0.0 to 1.0.1 (#327) 2019-12-01 13:19:48 +01:00
dependabot-preview[bot]
7a729d558f build(deps-dev): bump lockfile-lint from 2.2.0 to 3.0.1 (#316) 2019-11-27 15:09:59 +01:00
dependabot-preview[bot]
dc195a3446 build(deps-dev): bump lint-staged from 9.4.3 to 9.5.0 (#325) 2019-11-27 15:09:21 +01:00
dependabot-preview[bot]
a830403268 build(deps-dev): bump @types/react-router-dom from 5.1.2 to 5.1.3 (#326) 2019-11-27 15:08:54 +01:00
dependabot-preview[bot]
6a17a498e2 build(deps-dev): bump stylelint-processor-styled-components (#313) 2019-11-26 13:50:25 +01:00
dependabot-preview[bot]
a301eb0e8d build(deps-dev): bump eslint from 6.7.0 to 6.7.1 (#312) 2019-11-26 13:49:43 +01:00
dependabot-preview[bot]
de983f9a13 build(deps-dev): bump @typescript-eslint/parser from 2.8.0 to 2.9.0 (#321) 2019-11-26 13:46:55 +01:00
dependabot-preview[bot]
6da3204c0b build(deps-dev): bump @types/node from 12.12.11 to 12.12.14 (#319) 2019-11-26 13:45:42 +01:00
dependabot-preview[bot]
d4a17edc71 build(deps-dev): bump @types/validator from 12.0.0 to 12.0.1 (#320) 2019-11-26 13:42:27 +01:00
dependabot-preview[bot]
ba4299557e build(deps-dev): bump @types/react from 16.9.11 to 16.9.13 (#317) 2019-11-26 13:41:48 +01:00
Juan Picado @jotadeveloper
dc0cdbdb08 chore(release): 0.3.7 2019-11-24 21:05:59 +01:00
Priscila Oliveira
d955268c25 Revert "fix(#300): correctly reference registry url from options" (#311) 2019-11-24 20:17:28 +01:00
Michael Mok
ee74474811 fix(#300): correctly reference registry url from options 2019-11-24 19:43:19 +01:00
Priscila Oliveira
0d9232a92c Refactor(#209): Converted App component from class to func 2019-11-24 19:21:08 +01:00
Juan Picado @jotadeveloper
0a48906fc8 chore: enable optional chaining and nullish (#306) 2019-11-23 20:15:14 +01:00
dependabot-preview[bot]
58cf730b98 build(deps-dev): bump lint-staged from 8.2.1 to 9.4.3 (#289)
* build(deps-dev): bump lint-staged from 8.2.1 to 9.4.3

Bumps [lint-staged](https://github.com/okonet/lint-staged) from 8.2.1 to 9.4.3.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v8.2.1...v9.4.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

* fix: updated lint-staged conf
2019-11-23 15:40:24 +01:00
100 changed files with 9628 additions and 2684 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,36 @@
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. 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.
### [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.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) ### [0.3.5](https://github.com/verdaccio/ui/compare/v0.3.4...v0.3.5) (2019-11-07)

View File

@@ -6,6 +6,7 @@ import 'raf/polyfill';
import { configure } from 'enzyme'; import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
import { GlobalWithFetchMock } from 'jest-fetch-mock'; import { GlobalWithFetchMock } from 'jest-fetch-mock';
import 'mutationobserver-shim';
// @ts-ignore : Only a void function can be called with the 'new' keyword // @ts-ignore : Only a void function can be called with the 'new' keyword
configure({ adapter: new Adapter() }); configure({ adapter: new Adapter() });

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@verdaccio/ui-theme", "name": "@verdaccio/ui-theme",
"version": "0.3.6", "version": "0.3.9",
"description": "Verdaccio User Interface", "description": "Verdaccio User Interface",
"author": { "author": {
"name": "Verdaccio Core Team", "name": "Verdaccio Core Team",
@@ -13,56 +13,59 @@
"homepage": "https://verdaccio.org", "homepage": "https://verdaccio.org",
"main": "index.js", "main": "index.js",
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-nullish-coalescing-operator": "7.7.4",
"@babel/plugin-proposal-optional-chaining": "7.7.5",
"@commitlint/cli": "8.2.0", "@commitlint/cli": "8.2.0",
"@commitlint/config-conventional": "8.2.0", "@commitlint/config-conventional": "8.2.0",
"@emotion/core": "10.0.22", "@emotion/core": "10.0.22",
"@emotion/styled": "10.0.23", "@emotion/styled": "10.0.23",
"@material-ui/core": "4.6.1", "@material-ui/core": "4.7.2",
"@material-ui/icons": "4.5.1", "@material-ui/icons": "4.5.1",
"@octokit/rest": "16.35.0", "@octokit/rest": "16.35.0",
"@testing-library/react": "9.3.2", "@testing-library/jest-dom": "4.2.4",
"@testing-library/react": "9.3.3",
"@types/autosuggest-highlight": "3.1.0", "@types/autosuggest-highlight": "3.1.0",
"@types/enzyme": "3.10.3", "@types/enzyme": "3.10.4",
"@types/jest": "24.0.23", "@types/jest": "24.0.23",
"@types/js-base64": "2.3.1", "@types/js-base64": "2.3.1",
"@types/lodash": "4.14.149", "@types/lodash": "4.14.149",
"@types/node": "12.12.11", "@types/node": "12.12.17",
"@types/react": "16.9.11", "@types/react": "16.9.16",
"@types/react-autosuggest": "9.3.13", "@types/react-autosuggest": "9.3.13",
"@types/react-dom": "16.9.4", "@types/react-dom": "16.9.4",
"@types/react-router-dom": "5.1.2", "@types/react-router-dom": "5.1.3",
"@types/request": "2.48.3", "@types/request": "2.48.3",
"@types/validator": "12.0.0", "@types/validator": "12.0.1",
"@types/webpack-env": "1.14.1", "@types/webpack-env": "1.14.1",
"@typescript-eslint/parser": "2.8.0", "@typescript-eslint/parser": "2.11.0",
"@verdaccio/babel-preset": "8.2.0", "@verdaccio/babel-preset": "8.4.2",
"@verdaccio/commons-api": "8.3.0", "@verdaccio/commons-api": "8.4.2",
"@verdaccio/eslint-config": "8.2.0", "@verdaccio/eslint-config": "8.4.2",
"@verdaccio/types": "8.3.0", "@verdaccio/types": "8.4.2",
"autosuggest-highlight": "3.1.1", "autosuggest-highlight": "3.1.1",
"babel-loader": "8.0.6", "babel-loader": "8.0.6",
"bundlesize": "0.18.0", "bundlesize": "0.18.0",
"codeceptjs": "2.3.5", "codeceptjs": "2.3.6",
"codecov": "3.6.1", "codecov": "3.6.1",
"concurrently": "5.0.0", "concurrently": "5.0.1",
"cross-env": "6.0.3", "cross-env": "6.0.3",
"css-loader": "3.2.0", "css-loader": "3.3.0",
"date-fns": "2.8.1", "dayjs": "1.8.17",
"detect-secrets": "1.0.5", "detect-secrets": "1.0.5",
"emotion": "10.0.23", "emotion": "10.0.23",
"emotion-theming": "10.0.19", "emotion-theming": "10.0.19",
"enzyme": "3.10.0", "enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.1", "enzyme-adapter-react-16": "1.15.1",
"enzyme-to-json": "3.4.3", "enzyme-to-json": "3.4.3",
"eslint": "6.6.0", "eslint": "6.7.2",
"eslint-plugin-codeceptjs": "1.2.0", "eslint-plugin-codeceptjs": "1.2.0",
"eslint-plugin-import": "2.18.2", "eslint-plugin-import": "2.19.1",
"eslint-plugin-jsx-a11y": "6.2.3", "eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-prettier": "3.1.1", "eslint-plugin-prettier": "3.1.1",
"eslint-plugin-react": "7.16.0", "eslint-plugin-react": "7.17.0",
"eslint-plugin-react-hooks": "2.3.0", "eslint-plugin-react-hooks": "2.3.0",
"eslint-plugin-verdaccio": "8.2.0", "eslint-plugin-verdaccio": "8.4.2",
"file-loader": "4.3.0", "file-loader": "5.0.2",
"friendly-errors-webpack-plugin": "1.7.0", "friendly-errors-webpack-plugin": "1.7.0",
"get-stdin": "7.0.0", "get-stdin": "7.0.0",
"github-markdown-css": "3.0.1", "github-markdown-css": "3.0.1",
@@ -71,18 +74,19 @@
"identity-obj-proxy": "3.0.0", "identity-obj-proxy": "3.0.0",
"in-publish": "2.0.0", "in-publish": "2.0.0",
"jest": "24.9.0", "jest": "24.9.0",
"jest-emotion": "10.0.17", "jest-emotion": "10.0.26",
"jest-environment-jsdom": "24.9.0", "jest-environment-jsdom": "24.9.0",
"jest-environment-jsdom-global": "1.2.0", "jest-environment-jsdom-global": "1.2.0",
"jest-environment-node": "24.9.0", "jest-environment-node": "24.9.0",
"jest-fetch-mock": "2.1.2", "jest-fetch-mock": "2.1.2",
"js-base64": "2.5.1", "js-base64": "2.5.1",
"js-yaml": "3.13.1", "js-yaml": "3.13.1",
"lint-staged": "8.2.1", "lint-staged": "9.5.0",
"localstorage-memory": "1.0.3", "localstorage-memory": "1.0.3",
"lockfile-lint": "2.2.0", "lockfile-lint": "3.0.3",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mini-css-extract-plugin": "0.8.0", "mini-css-extract-plugin": "0.8.0",
"mutationobserver-shim": "0.3.3",
"node-mocks-http": "1.8.0", "node-mocks-http": "1.8.0",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"optimize-css-assets-webpack-plugin": "5.0.3", "optimize-css-assets-webpack-plugin": "5.0.3",
@@ -93,6 +97,7 @@
"react": "16.12.0", "react": "16.12.0",
"react-autosuggest": "9.4.3", "react-autosuggest": "9.4.3",
"react-dom": "16.12.0", "react-dom": "16.12.0",
"react-hook-form": "3.28.15",
"react-hot-loader": "4.12.18", "react-hot-loader": "4.12.18",
"react-router-dom": "5.1.2", "react-router-dom": "5.1.2",
"request": "2.88.0", "request": "2.88.0",
@@ -100,21 +105,21 @@
"rimraf": "3.0.0", "rimraf": "3.0.0",
"source-map-loader": "0.2.4", "source-map-loader": "0.2.4",
"standard-version": "7.0.1", "standard-version": "7.0.1",
"style-loader": "1.0.0", "style-loader": "1.0.1",
"stylelint": "12.0.0", "stylelint": "12.0.0",
"stylelint-config-recommended": "3.0.0", "stylelint-config-recommended": "3.0.0",
"stylelint-config-styled-components": "0.1.1", "stylelint-config-styled-components": "0.1.1",
"stylelint-processor-styled-components": "1.8.0", "stylelint-processor-styled-components": "1.9.0",
"stylelint-webpack-plugin": "1.1.0", "stylelint-webpack-plugin": "1.1.2",
"supertest": "4.0.2", "supertest": "4.0.2",
"typeface-roboto": "0.0.75", "typeface-roboto": "0.0.75",
"typescript": "3.7.2", "typescript": "3.7.3",
"uglifyjs-webpack-plugin": "2.2.0", "uglifyjs-webpack-plugin": "2.2.0",
"url-loader": "2.3.0", "url-loader": "3.0.0",
"validator": "12.1.0", "validator": "12.1.0",
"verdaccio": "4.3.5", "verdaccio": "4.3.5",
"verdaccio-auth-memory": "8.3.0", "verdaccio-auth-memory": "8.4.2",
"verdaccio-memory": "8.3.0", "verdaccio-memory": "8.4.2",
"wait-on": "3.3.0", "wait-on": "3.3.0",
"webpack": "4.41.2", "webpack": "4.41.2",
"webpack-bundle-analyzer": "3.6.0", "webpack-bundle-analyzer": "3.6.0",
@@ -133,7 +138,7 @@
"bundlesize": [ "bundlesize": [
{ {
"path": "./static/vendors.*.js", "path": "./static/vendors.*.js",
"maxSize": "180 kB" "maxSize": "185 kB"
}, },
{ {
"path": "./static/main.*.js", "path": "./static/main.*.js",
@@ -181,29 +186,23 @@
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run verdaccio:server\"" "dev": "concurrently --kill-others \"npm run dev:web\" \"npm run verdaccio:server\""
}, },
"engines": { "engines": {
"node": ">=8", "node": ">= 10.13.0",
"npm": ">=5" "npm": ">=5"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "lint-staged", "pre-commit": "lint-staged --relative",
"commit-msg": "commitlint -e $GIT_PARAMS" "commit-msg": "commitlint -e $GIT_PARAMS"
} }
}, },
"lint-staged": { "lint-staged": {
"relative": true, "*.{js,tsx,ts}": [
"linters": { "eslint . --ext .js,.ts,.tsx",
"*.{js,tsx,ts}": [ "prettier --write"
"eslint . --ext .js,.ts,.tsx", ],
"prettier --write" "*": [
], "detect-secrets-launcher --baseline .secrets-baseline",
"*": [ "git add"
"detect-secrets-launcher --baseline .secrets-baseline",
"git add"
]
},
"ignore": [
"*.json"
] ]
}, },
"license": "MIT", "license": "MIT",
@@ -216,6 +215,5 @@
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/verdaccio", "url": "https://opencollective.com/verdaccio",
"logo": "https://opencollective.com/verdaccio/logo.txt" "logo": "https://opencollective.com/verdaccio/logo.txt"
}, }
"dependencies": {}
} }

View File

@@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import { ReactWrapper } from 'enzyme';
import { mount } from '../utils/test-enzyme'; import { render, waitForElement, fireEvent } from '../utils/test-react-testing-library';
import storage from '../utils/storage'; import storage from '../utils/storage';
// eslint-disable-next-line jest/no-mocks-import
import { generateTokenWithTimeRange } from '../../jest/unit/components/__mocks__/token'; import { generateTokenWithTimeRange } from '../../jest/unit/components/__mocks__/token';
import App from './App'; import App from './App';
import { AppProps } from './AppContext';
jest.mock('../utils/storage', () => { jest.mock('../utils/storage', () => {
class LocalStorageMock { class LocalStorageMock {
@@ -31,66 +30,75 @@ jest.mock('../utils/storage', () => {
}); });
jest.mock('../utils/api', () => ({ jest.mock('../utils/api', () => ({
// eslint-disable-next-line jest/no-mocks-import
request: require('../../jest/unit/components/__mocks__/api').default.request, request: require('../../jest/unit/components/__mocks__/api').default.request,
})); }));
describe('App', () => { /* eslint-disable react/jsx-no-bind*/
let wrapper: ReactWrapper<{}, AppProps, App>; describe('<App />', () => {
test('should display the Loading component at the beginning ', () => {
const { container, queryByTestId } = render(<App />);
beforeEach(() => { expect(container.firstChild).toMatchSnapshot();
wrapper = mount(<App />); expect(queryByTestId('loading')).toBeTruthy();
}); });
test('toggleLoginModal: should toggle the value in state', () => { test('should display the Header component ', async () => {
const { handleToggleLoginModal } = wrapper.instance(); const { container, queryByTestId } = render(<App />);
expect(wrapper.state().showLoginModal).toBeFalsy();
handleToggleLoginModal(); expect(container.firstChild).toMatchSnapshot();
expect(wrapper.state('showLoginModal')).toBeTruthy(); expect(queryByTestId('loading')).toBeTruthy();
expect(wrapper.state('error')).toEqual(undefined);
// 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('isUserAlreadyLoggedIn: token already available in storage', async () => { test('isUserAlreadyLoggedIn: token already available in storage', async () => {
storage.setItem('username', 'verdaccio'); storage.setItem('username', 'verdaccio');
storage.setItem('token', generateTokenWithTimeRange(24)); storage.setItem('token', generateTokenWithTimeRange(24));
const { isUserAlreadyLoggedIn } = wrapper.instance();
isUserAlreadyLoggedIn(); const { queryByTestId, queryAllByText } = render(<App />);
expect(wrapper.state('user').username).toEqual('verdaccio'); // wait for the Account's circle element component appearance and return the element
}); const accountCircleElement = await waitForElement(() => queryByTestId('header--menu-accountcircle'));
expect(accountCircleElement).toBeTruthy();
test('handleLogout - logouts the user and clear localstorage', async () => { if (accountCircleElement) {
const { handleLogout } = wrapper.instance(); fireEvent.click(accountCircleElement);
storage.setItem('username', 'verdaccio');
storage.setItem('token', 'xxxx.TOKEN.xxxx');
await handleLogout(); // wait for the Greeting's label element component appearance and return the element
expect(wrapper.state('user')).toEqual({}); const greetingsLabelElement = await waitForElement(() => queryByTestId('greetings-label'));
expect(wrapper.state('isUserLoggedIn')).toBeFalsy(); expect(greetingsLabelElement).toBeTruthy();
});
test('handleDoLogin - login the user successfully', async () => { if (greetingsLabelElement) {
const { handleDoLogin } = wrapper.instance(); expect(queryAllByText('verdaccio')).toBeTruthy();
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,184 +1,104 @@
import React, { Component, ReactElement } from 'react'; import React, { useState, useEffect } from 'react';
import styled from '@emotion/styled';
import isNil from 'lodash/isNil'; import isNil from 'lodash/isNil';
import { Router } from 'react-router-dom';
import storage from '../utils/storage'; import storage from '../utils/storage';
import { makeLogin, isTokenExpire } from '../utils/login'; import { isTokenExpire } from '../utils/login';
import Loading from '../components/Loading';
import LoginModal from '../components/Login';
import Header from '../components/Header';
import { Container, Content } from '../components/Layout';
import API from '../utils/api'; import API from '../utils/api';
import Header from '../components/Header';
import Footer from '../components/Footer'; import Footer from '../components/Footer';
import Box from '../muiComponents/Box';
import Loading from '../components/Loading';
import StyleBaseline from '../design-tokens/StyleBaseline';
import { Theme } from '../design-tokens/theme';
import AppRoute from './AppRoute'; import AppContextProvider from './AppContextProvider';
import { AppProps, AppContextProvider } from './AppContext'; import AppRoute, { history } from './AppRoute';
export default class App extends Component<{}, AppProps> { const StyledBoxContent = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
public state: AppProps = { padding: 15,
logoUrl: window.VERDACCIO_LOGO, [`@media screen and (min-width: ${theme && theme.breakPoints.container}px)`]: {
user: {}, maxWidth: theme && theme.breakPoints.container,
scope: window.VERDACCIO_SCOPE || '', width: '100%',
showLoginModal: false, marginLeft: 'auto',
isUserLoggedIn: false, marginRight: 'auto',
packages: [], },
isLoading: true, }));
/* eslint-disable react/jsx-no-bind */
/* eslint-disable react-hooks/exhaustive-deps */
const App: React.FC = () => {
const [user, setUser] = useState();
const [packages, setPackages] = useState([]);
const [isLoading, setIsLoading] = useState(true);
/**
* Logouts user
* Required by: <Header />
*/
const logout = () => {
storage.removeItem('username');
storage.removeItem('token');
setUser(undefined);
}; };
public componentDidMount(): void { const checkUserAlreadyLoggedIn = () => {
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 // checks for token validity
const token = storage.getItem('token'); const token = storage.getItem('token');
const username: string = storage.getItem('username') as string; const username = storage.getItem('username');
if (isTokenExpire(token) || isNil(username)) { if (isTokenExpire(token) || isNil(username)) {
this.handleLogout(); logout();
} else { return;
this.setState({
user: { username },
isUserLoggedIn: true,
});
} }
setUser({ username });
}; };
public loadOnHandler = async () => { const loadOnHandler = async () => {
try { try {
const packages = await API.request<any[]>('packages', 'GET'); const packages = await API.request('packages', 'GET');
// @ts-ignore: FIX THIS TYPE: Type 'any[]' is not assignable to type '[]' // FIXME add correct type for package
this.setState({ setPackages(packages as never[]);
packages,
isLoading: false,
});
} catch (error) { } catch (error) {
// FIXME: add dialog // FIXME: add dialog
console.error({ console.error({
title: 'Warning', title: 'Warning',
message: `Unable to load package list: ${error.message}`, message: `Unable to load package list: ${error.message}`,
}); });
this.setLoading(false);
}
};
public setLoading = (isLoading: boolean) =>
this.setState({
isLoading,
});
/**
* 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) { setIsLoading(false);
this.setState({
user: {},
error,
});
}
}; };
public setLoggedUser = (username: string) => { useEffect(() => {
this.setState({ checkUserAlreadyLoggedIn();
user: { loadOnHandler();
username, }, []);
},
isUserLoggedIn: true, // close login modal after successful login
showLoginModal: false, // set isUserLoggedIn to true
});
};
/** return (
* Logouts user <>
* Required by: <Header /> <StyleBaseline />
*/ <Box display="flex" flexDirection="column" height="100%">
public handleLogout = () => { {isLoading ? (
storage.removeItem('username'); <Loading />
storage.removeItem('token'); ) : (
this.setState({ <>
user: {}, <Router history={history}>
isUserLoggedIn: false, <AppContextProvider packages={packages} user={user}>
}); <Header />
}; <StyledBoxContent flexGrow={1}>
<AppRoute />
</StyledBoxContent>
</AppContextProvider>
</Router>
<Footer />
</>
)}
</Box>
</>
);
};
public renderLoginModal = (): ReactElement<HTMLElement> => { export default App;
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}
/>
);
};
}

19
src/App/AppContext.ts Normal file
View File

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

View File

@@ -1,20 +0,0 @@
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

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

View File

@@ -4,7 +4,7 @@ import { createBrowserHistory } from 'history';
import Loading from '../components/Loading'; import Loading from '../components/Loading';
import { AppContext } from './AppContext'; import AppContext from './AppContext';
const NotFound = lazy(() => import('../components/NotFound')); const NotFound = lazy(() => import('../components/NotFound'));
const VersionContextProvider = lazy(() => import('../pages/Version/VersionContextProvider')); const VersionContextProvider = lazy(() => import('../pages/Version/VersionContextProvider'));
@@ -19,19 +19,24 @@ enum Route {
PACKAGE_VERSION = '/-/web/detail/:package/v/:version', PACKAGE_VERSION = '/-/web/detail/:package/v/:version',
} }
const history = createBrowserHistory({ export const history = createBrowserHistory({
basename: window.__VERDACCIO_BASENAME_UI_OPTIONS && window.__VERDACCIO_BASENAME_UI_OPTIONS.url_prefix, basename: window.__VERDACCIO_BASENAME_UI_OPTIONS && window.__VERDACCIO_BASENAME_UI_OPTIONS.url_prefix,
}); });
/* eslint react/jsx-max-depth: 0 */ const AppRoute: React.FC = () => {
const AppRoute: React.FC = ({ children }) => {
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
const { isUserLoggedIn, packages } = appContext;
if (!appContext) {
throw Error('The app Context was not correct used');
}
const { user, packages } = appContext;
const isUserLoggedIn = user && user.username;
return ( return (
<Router history={history}> <Router history={history}>
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
{children}
<Switch> <Switch>
<ReactRouterDomRoute exact={true} path={Route.ROOT}> <ReactRouterDomRoute exact={true} path={Route.ROOT}>
<HomePage isUserLoggedIn={!!isUserLoggedIn} packages={packages || []} /> <HomePage isUserLoggedIn={!!isUserLoggedIn} packages={packages || []} />

View File

@@ -0,0 +1,179 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<App /> should display the Header component 1`] = `
.emotion-8 {
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
top: 50%;
left: 50%;
position: absolute;
}
.emotion-2 {
margin: 0 0 30px 0;
border-radius: 25px;
box-shadow: 0 10px 20px 0 rgba(69,58,100,0.2);
background: #f7f8f6;
}
.emotion-0 {
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
background-position: center;
background-size: contain;
background-image: url([object Object]);
background-repeat: no-repeat;
width: 90px;
height: 90px;
}
.emotion-6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.emotion-4 {
color: #4b5e40;
}
<div
class="MuiBox-root MuiBox-root-218"
>
<div
class="emotion-8 emotion-9"
data-testid="loading"
>
<div
class="emotion-2 emotion-3"
>
<div
class="emotion-0 emotion-1"
/>
</div>
<div
class="emotion-6 emotion-7"
>
<div
class="MuiCircularProgress-root emotion-4 emotion-5 MuiCircularProgress-colorPrimary MuiCircularProgress-indeterminate"
role="progressbar"
style="width: 50px; height: 50px;"
>
<svg
class="MuiCircularProgress-svg"
viewBox="22 22 44 44"
>
<circle
class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate"
cx="44"
cy="44"
fill="none"
r="20.2"
stroke-width="3.6"
/>
</svg>
</div>
</div>
</div>
</div>
`;
exports[`<App /> should display the Loading component at the beginning 1`] = `
.emotion-8 {
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
top: 50%;
left: 50%;
position: absolute;
}
.emotion-2 {
margin: 0 0 30px 0;
border-radius: 25px;
box-shadow: 0 10px 20px 0 rgba(69,58,100,0.2);
background: #f7f8f6;
}
.emotion-0 {
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
background-position: center;
background-size: contain;
background-image: url([object Object]);
background-repeat: no-repeat;
width: 90px;
height: 90px;
}
.emotion-6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.emotion-4 {
color: #4b5e40;
}
<div
class="MuiBox-root MuiBox-root-2"
>
<div
class="emotion-8 emotion-9"
data-testid="loading"
>
<div
class="emotion-2 emotion-3"
>
<div
class="emotion-0 emotion-1"
/>
</div>
<div
class="emotion-6 emotion-7"
>
<div
class="MuiCircularProgress-root emotion-4 emotion-5 MuiCircularProgress-colorPrimary MuiCircularProgress-indeterminate"
role="progressbar"
style="width: 50px; height: 50px;"
>
<svg
class="MuiCircularProgress-svg"
viewBox="22 22 44 44"
>
<circle
class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate"
cx="44"
cy="44"
fill="none"
r="20.2"
stroke-width="3.6"
/>
</svg>
</div>
</div>
</div>
</div>
`;

View File

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

View File

@@ -1,89 +1,71 @@
import React from 'react'; import React from 'react';
import { mount } from '../../utils/test-enzyme'; import { render, cleanup } from '../../utils/test-react-testing-library';
import api from '../../utils/api'; import { DetailContext, DetailContextProps } from '../../pages/Version';
import { ActionBar } from './ActionBar'; import ActionBar from './ActionBar';
const mockPackageMeta: jest.Mock = jest.fn(() => ({ const detailContextValue: DetailContextProps = {
latest: { packageName: 'foo',
homepage: 'https://verdaccio.tld', readMe: 'test',
bugs: { enableLoading: () => {},
url: 'https://verdaccio.tld/bugs', isLoading: false,
}, hasNotBeenFound: false,
dist: { packageMeta: {
tarball: 'https://verdaccio.tld/download', _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',
},
}, },
}, },
})); };
jest.mock('../../pages/Version', () => ({ const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({ contextValue }) => (
DetailContextConsumer: component => { <DetailContext.Provider value={contextValue}>
return component.children({ packageMeta: mockPackageMeta() }); <ActionBar />
}, </DetailContext.Provider>
})); );
describe('<ActionBar /> component', () => { describe('<ActionBar /> component', () => {
beforeEach(() => { afterEach(() => {
jest.resetModules(); cleanup();
jest.resetAllMocks();
}); });
test('should render the component in default state', () => { test('should render the component in default state', () => {
const wrapper = mount(<ActionBar />); const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(wrapper.html()).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
test('when there is no action bar data', () => { test('when there is no action bar data', () => {
mockPackageMeta.mockImplementation(() => ({ const packageMeta = {
latest: {}, ...detailContextValue.packageMeta,
})); latest: {
...detailContextValue.packageMeta.latest,
homepage: undefined,
bugs: undefined,
dist: {
...detailContextValue.packageMeta.latest.dist,
tarball: undefined,
},
},
};
const wrapper = mount(<ActionBar />); const { container } = render(<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />);
// FIXME: this only renders the DetailContextConsumer, thus expect(container.firstChild).toMatchSnapshot();
// 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', () => { test('when there is a button to download a tarball', () => {
mockPackageMeta.mockImplementation(() => ({ const { getByTitle } = render(<ComponentToBeRendered contextValue={{ ...detailContextValue }} />);
latest: { expect(getByTitle('Download tarball')).toBeTruthy();
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', () => { test('when there is a button to open an issue', () => {
mockPackageMeta.mockImplementation(() => ({ const { getByTitle } = render(<ComponentToBeRendered contextValue={{ ...detailContextValue }} />);
latest: { expect(getByTitle('Open an issue')).toBeTruthy();
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,133 +1,44 @@
import React, { Component, ReactElement } from 'react'; import React from 'react';
import BugReportIcon from '@material-ui/icons/BugReport';
import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/Version'; import { DetailContext } from '../../pages/Version';
import { isURL, extractFileName, downloadFile } from '../../utils/url'; import { isURL } from '../../utils/url';
import api from '../../utils/api'; import Box from '../../muiComponents/Box';
import Tooltip from '../../muiComponents/Tooltip';
import List from '../../muiComponents/List';
import { Fab, ActionListItem } from './styles'; import ActionBarAction, { ActionBarActionProps } from './ActionBarAction';
export interface Action { /* eslint-disable verdaccio/jsx-spread */
icon: string; const ActionBar: React.FC = () => {
title: string; const detailContext = React.useContext(DetailContext);
handler?: Function;
}
export async function downloadHandler(link: string): Promise<void> { const { packageMeta } = detailContext;
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);
}
const ACTIONS = { if (!packageMeta?.latest) {
homepage: { return null;
icon: <HomeIcon />, }
title: 'Visit homepage',
}, const { homepage, bugs, dist } = packageMeta.latest;
issue: {
icon: <BugReportIcon />, const actions: Array<ActionBarActionProps> = [];
title: 'Open an issue',
}, if (homepage && isURL(homepage)) {
tarball: { actions.push({ type: 'VISIT_HOMEPAGE', link: homepage });
icon: <DownloadIcon />, }
title: 'Download tarball',
handler: downloadHandler, 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>
);
}; };
class ActionBar extends Component { export default ActionBar;
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

@@ -0,0 +1,61 @@
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 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 }) => {
switch (type) {
case 'VISIT_HOMEPAGE':
return (
<Tooltip title="Visit homepage">
<Link external={true} to={link}>
<Fab size="small">
<HomeIcon />
</Fab>
</Link>
</Tooltip>
);
case 'OPEN_AN_ISSUE':
return (
<Tooltip title="Open an issue">
<Link external={true} to={link}>
<Fab size="small">
<BugReportIcon />
</Fab>
</Link>
</Tooltip>
);
case 'DOWNLOAD_TARBALL':
return (
<Tooltip title="Download tarball">
<Fab data-testid="download-tarball-btn" onClick={downloadTarball(link)} size="small">
<DownloadIcon />
</Fab>
</Tooltip>
);
}
};
export default ActionBarAction;

View File

@@ -1,7 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ActionBar /> component should render the component in default state 1`] = `""`; exports[`<ActionBar /> component should render the component in default state 1`] = `
.emotion-0 {
background-color: #4b5e40;
color: #fff;
margin-right: 10px;
}
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>"`; <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 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>"`; exports[`<ActionBar /> component when there is no action bar data 1`] = `
<div
class="MuiBox-root MuiBox-root-77"
/>
`;

View File

@@ -0,0 +1,18 @@
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 +1,2 @@
export { default } from './ActionBar'; export { default } from './ActionBar';
export { default as downloadTarball } from './download-tarball';

View File

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

View File

@@ -1,16 +1,16 @@
import React, { KeyboardEvent } from 'react'; import React, { KeyboardEvent, memo } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import Autosuggest, { SuggestionSelectedEventData, InputProps, ChangeEvent } from 'react-autosuggest'; import Autosuggest, { SuggestionSelectedEventData, InputProps, ChangeEvent } from 'react-autosuggest';
import match from 'autosuggest-highlight/match'; import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse'; import parse from 'autosuggest-highlight/parse';
import { fontWeight } from '../../utils/styles/sizes';
import MenuItem from '../../muiComponents/MenuItem'; import MenuItem from '../../muiComponents/MenuItem';
import { Theme } from '../../design-tokens/theme';
import { Wrapper, InputField, SuggestionContainer } from './styles'; import { Wrapper, InputField, SuggestionContainer } from './styles';
const StyledAnchor = styled('a')<{ fw: number }>(props => ({ const StyledAnchor = styled('a')<{ highlight: boolean; theme?: Theme }>(props => ({
fontWeight: props.fw, fontWeight: props.theme && props.highlight ? props.theme.fontWeight.semiBold : props.theme.fontWeight.light,
})); }));
const StyledMenuItem = styled(MenuItem)({ const StyledMenuItem = styled(MenuItem)({
@@ -64,9 +64,8 @@ const renderSuggestion = (suggestion, { query, isHighlighted }): JSX.Element =>
<StyledMenuItem component="div" selected={isHighlighted}> <StyledMenuItem component="div" selected={isHighlighted}>
<div> <div>
{parts.map((part, index) => { {parts.map((part, index) => {
const fw = part.highlight ? fontWeight.semiBold : fontWeight.light;
return ( return (
<StyledAnchor fw={fw} key={String(index)}> <StyledAnchor highlight={part.highlight} key={String(index)}>
{part.text} {part.text}
</StyledAnchor> </StyledAnchor>
); );
@@ -90,64 +89,66 @@ const SUGGESTIONS_RESPONSE = {
NO_RESULT: 'No results found.', NO_RESULT: 'No results found.',
}; };
const AutoComplete = ({ const AutoComplete = memo(
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, 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, startAdornment,
disableUnderline, onChange,
onSuggestionsFetch,
onCleanSuggestions,
value = '',
placeholder = '',
disableUnderline = false,
onClick,
onKeyDown, onKeyDown,
onBlur, onBlur,
}; suggestionsLoading = false,
suggestionsLoaded = false,
suggestionsError = false,
}: Props) => {
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(SUGGESTIONS_RESPONSE.NO_RESULT)}
{suggestionsLoading && query && renderMessage(SUGGESTIONS_RESPONSE.LOADING)}
{suggestionsError && renderMessage(SUGGESTIONS_RESPONSE.FAILURE)}
{children}
</SuggestionContainer>
);
}
// this format avoid arrow function eslint rule
function renderSuggestionsContainer({ containerProps, children, query }): JSX.Element {
return ( return (
<SuggestionContainer {...containerProps} square={true}> <Wrapper>
{suggestionsLoaded && children === null && query && renderMessage(SUGGESTIONS_RESPONSE.NO_RESULT)} <Autosuggest
{suggestionsLoading && query && renderMessage(SUGGESTIONS_RESPONSE.LOADING)} {...autosuggestProps}
{suggestionsError && renderMessage(SUGGESTIONS_RESPONSE.FAILURE)} inputProps={inputProps}
{children} onSuggestionSelected={onClick}
</SuggestionContainer> renderSuggestionsContainer={renderSuggestionsContainer}
/>
</Wrapper>
); );
} }
);
return (
<Wrapper>
<Autosuggest
{...autosuggestProps}
inputProps={inputProps}
onSuggestionSelected={onClick}
renderSuggestionsContainer={renderSuggestionsContainer}
/>
</Wrapper>
);
};
export default AutoComplete; export default AutoComplete;

View File

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

View File

@@ -1,6 +1,6 @@
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { ActionBar } from '../ActionBar/ActionBar'; import ActionBar from '../ActionBar';
import Author from '../Author'; import Author from '../Author';
import Developers from '../Developers'; import Developers from '../Developers';
import Dist from '../Dist/Dist'; import Dist from '../Dist/Dist';

View File

@@ -122,9 +122,12 @@ exports[`test Developers should render the component for contributors with items
<ForwardRef(Tooltip) <ForwardRef(Tooltip)
classes={ classes={
Object { Object {
"arrow": "MuiTooltip-arrow",
"popper": "MuiTooltip-popper", "popper": "MuiTooltip-popper",
"popperArrow": "MuiTooltip-popperArrow",
"popperInteractive": "MuiTooltip-popperInteractive", "popperInteractive": "MuiTooltip-popperInteractive",
"tooltip": "MuiTooltip-tooltip", "tooltip": "MuiTooltip-tooltip",
"tooltipArrow": "MuiTooltip-tooltipArrow",
"tooltipPlacementBottom": "MuiTooltip-tooltipPlacementBottom", "tooltipPlacementBottom": "MuiTooltip-tooltipPlacementBottom",
"tooltipPlacementLeft": "MuiTooltip-tooltipPlacementLeft", "tooltipPlacementLeft": "MuiTooltip-tooltipPlacementLeft",
"tooltipPlacementRight": "MuiTooltip-tooltipPlacementRight", "tooltipPlacementRight": "MuiTooltip-tooltipPlacementRight",
@@ -159,6 +162,7 @@ exports[`test Developers should render the component for contributors with items
Object { Object {
"circle": "MuiAvatar-circle", "circle": "MuiAvatar-circle",
"colorDefault": "MuiAvatar-colorDefault", "colorDefault": "MuiAvatar-colorDefault",
"fallback": "MuiAvatar-fallback",
"img": "MuiAvatar-img", "img": "MuiAvatar-img",
"root": "MuiAvatar-root", "root": "MuiAvatar-root",
"rounded": "MuiAvatar-rounded", "rounded": "MuiAvatar-rounded",
@@ -169,7 +173,44 @@ exports[`test Developers should render the component for contributors with items
<div <div
aria-label="dmethvin" aria-label="dmethvin"
className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault" className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
/> >
<ForwardRef
className="MuiAvatar-fallback"
>
<WithStyles(ForwardRef(SvgIcon))
className="MuiAvatar-fallback"
>
<ForwardRef(SvgIcon)
className="MuiAvatar-fallback"
classes={
Object {
"colorAction": "MuiSvgIcon-colorAction",
"colorDisabled": "MuiSvgIcon-colorDisabled",
"colorError": "MuiSvgIcon-colorError",
"colorPrimary": "MuiSvgIcon-colorPrimary",
"colorSecondary": "MuiSvgIcon-colorSecondary",
"fontSizeInherit": "MuiSvgIcon-fontSizeInherit",
"fontSizeLarge": "MuiSvgIcon-fontSizeLarge",
"fontSizeSmall": "MuiSvgIcon-fontSizeSmall",
"root": "MuiSvgIcon-root",
}
}
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root MuiAvatar-fallback"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</ForwardRef(SvgIcon)>
</WithStyles(ForwardRef(SvgIcon))>
</ForwardRef>
</div>
</ForwardRef(Avatar)> </ForwardRef(Avatar)>
</WithStyles(ForwardRef(Avatar))> </WithStyles(ForwardRef(Avatar))>
</ForwardRef(Avatar)> </ForwardRef(Avatar)>
@@ -185,13 +226,35 @@ exports[`test Developers should render the component for contributors with items
<div <div
aria-label="dmethvin" aria-label="dmethvin"
class="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault" class="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
/> >
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiAvatar-fallback"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</div>
</a> </a>
} }
className="MuiTooltip-popper" className="MuiTooltip-popper"
id={null} id={null}
open={false} open={false}
placement="bottom" placement="bottom"
popperOptions={
Object {
"modifiers": Object {
"arrow": Object {
"element": null,
"enabled": false,
},
},
}
}
transition={true} transition={true}
/> />
</ForwardRef(Tooltip)> </ForwardRef(Tooltip)>
@@ -221,9 +284,12 @@ exports[`test Developers should render the component for contributors with items
<ForwardRef(Tooltip) <ForwardRef(Tooltip)
classes={ classes={
Object { Object {
"arrow": "MuiTooltip-arrow",
"popper": "MuiTooltip-popper", "popper": "MuiTooltip-popper",
"popperArrow": "MuiTooltip-popperArrow",
"popperInteractive": "MuiTooltip-popperInteractive", "popperInteractive": "MuiTooltip-popperInteractive",
"tooltip": "MuiTooltip-tooltip", "tooltip": "MuiTooltip-tooltip",
"tooltipArrow": "MuiTooltip-tooltipArrow",
"tooltipPlacementBottom": "MuiTooltip-tooltipPlacementBottom", "tooltipPlacementBottom": "MuiTooltip-tooltipPlacementBottom",
"tooltipPlacementLeft": "MuiTooltip-tooltipPlacementLeft", "tooltipPlacementLeft": "MuiTooltip-tooltipPlacementLeft",
"tooltipPlacementRight": "MuiTooltip-tooltipPlacementRight", "tooltipPlacementRight": "MuiTooltip-tooltipPlacementRight",
@@ -258,6 +324,7 @@ exports[`test Developers should render the component for contributors with items
Object { Object {
"circle": "MuiAvatar-circle", "circle": "MuiAvatar-circle",
"colorDefault": "MuiAvatar-colorDefault", "colorDefault": "MuiAvatar-colorDefault",
"fallback": "MuiAvatar-fallback",
"img": "MuiAvatar-img", "img": "MuiAvatar-img",
"root": "MuiAvatar-root", "root": "MuiAvatar-root",
"rounded": "MuiAvatar-rounded", "rounded": "MuiAvatar-rounded",
@@ -268,7 +335,44 @@ exports[`test Developers should render the component for contributors with items
<div <div
aria-label="mgol" aria-label="mgol"
className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault" className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
/> >
<ForwardRef
className="MuiAvatar-fallback"
>
<WithStyles(ForwardRef(SvgIcon))
className="MuiAvatar-fallback"
>
<ForwardRef(SvgIcon)
className="MuiAvatar-fallback"
classes={
Object {
"colorAction": "MuiSvgIcon-colorAction",
"colorDisabled": "MuiSvgIcon-colorDisabled",
"colorError": "MuiSvgIcon-colorError",
"colorPrimary": "MuiSvgIcon-colorPrimary",
"colorSecondary": "MuiSvgIcon-colorSecondary",
"fontSizeInherit": "MuiSvgIcon-fontSizeInherit",
"fontSizeLarge": "MuiSvgIcon-fontSizeLarge",
"fontSizeSmall": "MuiSvgIcon-fontSizeSmall",
"root": "MuiSvgIcon-root",
}
}
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root MuiAvatar-fallback"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</ForwardRef(SvgIcon)>
</WithStyles(ForwardRef(SvgIcon))>
</ForwardRef>
</div>
</ForwardRef(Avatar)> </ForwardRef(Avatar)>
</WithStyles(ForwardRef(Avatar))> </WithStyles(ForwardRef(Avatar))>
</ForwardRef(Avatar)> </ForwardRef(Avatar)>
@@ -284,13 +388,35 @@ exports[`test Developers should render the component for contributors with items
<div <div
aria-label="mgol" aria-label="mgol"
class="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault" class="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
/> >
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiAvatar-fallback"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</div>
</a> </a>
} }
className="MuiTooltip-popper" className="MuiTooltip-popper"
id={null} id={null}
open={false} open={false}
placement="bottom" placement="bottom"
popperOptions={
Object {
"modifiers": Object {
"arrow": Object {
"element": null,
"enabled": false,
},
},
}
}
transition={true} transition={true}
/> />
</ForwardRef(Tooltip)> </ForwardRef(Tooltip)>
@@ -426,9 +552,12 @@ exports[`test Developers should render the component for maintainers with items
<ForwardRef(Tooltip) <ForwardRef(Tooltip)
classes={ classes={
Object { Object {
"arrow": "MuiTooltip-arrow",
"popper": "MuiTooltip-popper", "popper": "MuiTooltip-popper",
"popperArrow": "MuiTooltip-popperArrow",
"popperInteractive": "MuiTooltip-popperInteractive", "popperInteractive": "MuiTooltip-popperInteractive",
"tooltip": "MuiTooltip-tooltip", "tooltip": "MuiTooltip-tooltip",
"tooltipArrow": "MuiTooltip-tooltipArrow",
"tooltipPlacementBottom": "MuiTooltip-tooltipPlacementBottom", "tooltipPlacementBottom": "MuiTooltip-tooltipPlacementBottom",
"tooltipPlacementLeft": "MuiTooltip-tooltipPlacementLeft", "tooltipPlacementLeft": "MuiTooltip-tooltipPlacementLeft",
"tooltipPlacementRight": "MuiTooltip-tooltipPlacementRight", "tooltipPlacementRight": "MuiTooltip-tooltipPlacementRight",
@@ -463,6 +592,7 @@ exports[`test Developers should render the component for maintainers with items
Object { Object {
"circle": "MuiAvatar-circle", "circle": "MuiAvatar-circle",
"colorDefault": "MuiAvatar-colorDefault", "colorDefault": "MuiAvatar-colorDefault",
"fallback": "MuiAvatar-fallback",
"img": "MuiAvatar-img", "img": "MuiAvatar-img",
"root": "MuiAvatar-root", "root": "MuiAvatar-root",
"rounded": "MuiAvatar-rounded", "rounded": "MuiAvatar-rounded",
@@ -473,7 +603,44 @@ exports[`test Developers should render the component for maintainers with items
<div <div
aria-label="dmethvin" aria-label="dmethvin"
className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault" className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
/> >
<ForwardRef
className="MuiAvatar-fallback"
>
<WithStyles(ForwardRef(SvgIcon))
className="MuiAvatar-fallback"
>
<ForwardRef(SvgIcon)
className="MuiAvatar-fallback"
classes={
Object {
"colorAction": "MuiSvgIcon-colorAction",
"colorDisabled": "MuiSvgIcon-colorDisabled",
"colorError": "MuiSvgIcon-colorError",
"colorPrimary": "MuiSvgIcon-colorPrimary",
"colorSecondary": "MuiSvgIcon-colorSecondary",
"fontSizeInherit": "MuiSvgIcon-fontSizeInherit",
"fontSizeLarge": "MuiSvgIcon-fontSizeLarge",
"fontSizeSmall": "MuiSvgIcon-fontSizeSmall",
"root": "MuiSvgIcon-root",
}
}
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root MuiAvatar-fallback"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</ForwardRef(SvgIcon)>
</WithStyles(ForwardRef(SvgIcon))>
</ForwardRef>
</div>
</ForwardRef(Avatar)> </ForwardRef(Avatar)>
</WithStyles(ForwardRef(Avatar))> </WithStyles(ForwardRef(Avatar))>
</ForwardRef(Avatar)> </ForwardRef(Avatar)>
@@ -489,13 +656,35 @@ exports[`test Developers should render the component for maintainers with items
<div <div
aria-label="dmethvin" aria-label="dmethvin"
class="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault" class="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
/> >
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiAvatar-fallback"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</div>
</a> </a>
} }
className="MuiTooltip-popper" className="MuiTooltip-popper"
id={null} id={null}
open={false} open={false}
placement="bottom" placement="bottom"
popperOptions={
Object {
"modifiers": Object {
"arrow": Object {
"element": null,
"enabled": false,
},
},
}
}
transition={true} transition={true}
/> />
</ForwardRef(Tooltip)> </ForwardRef(Tooltip)>
@@ -525,9 +714,12 @@ exports[`test Developers should render the component for maintainers with items
<ForwardRef(Tooltip) <ForwardRef(Tooltip)
classes={ classes={
Object { Object {
"arrow": "MuiTooltip-arrow",
"popper": "MuiTooltip-popper", "popper": "MuiTooltip-popper",
"popperArrow": "MuiTooltip-popperArrow",
"popperInteractive": "MuiTooltip-popperInteractive", "popperInteractive": "MuiTooltip-popperInteractive",
"tooltip": "MuiTooltip-tooltip", "tooltip": "MuiTooltip-tooltip",
"tooltipArrow": "MuiTooltip-tooltipArrow",
"tooltipPlacementBottom": "MuiTooltip-tooltipPlacementBottom", "tooltipPlacementBottom": "MuiTooltip-tooltipPlacementBottom",
"tooltipPlacementLeft": "MuiTooltip-tooltipPlacementLeft", "tooltipPlacementLeft": "MuiTooltip-tooltipPlacementLeft",
"tooltipPlacementRight": "MuiTooltip-tooltipPlacementRight", "tooltipPlacementRight": "MuiTooltip-tooltipPlacementRight",
@@ -562,6 +754,7 @@ exports[`test Developers should render the component for maintainers with items
Object { Object {
"circle": "MuiAvatar-circle", "circle": "MuiAvatar-circle",
"colorDefault": "MuiAvatar-colorDefault", "colorDefault": "MuiAvatar-colorDefault",
"fallback": "MuiAvatar-fallback",
"img": "MuiAvatar-img", "img": "MuiAvatar-img",
"root": "MuiAvatar-root", "root": "MuiAvatar-root",
"rounded": "MuiAvatar-rounded", "rounded": "MuiAvatar-rounded",
@@ -572,7 +765,44 @@ exports[`test Developers should render the component for maintainers with items
<div <div
aria-label="mgol" aria-label="mgol"
className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault" className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
/> >
<ForwardRef
className="MuiAvatar-fallback"
>
<WithStyles(ForwardRef(SvgIcon))
className="MuiAvatar-fallback"
>
<ForwardRef(SvgIcon)
className="MuiAvatar-fallback"
classes={
Object {
"colorAction": "MuiSvgIcon-colorAction",
"colorDisabled": "MuiSvgIcon-colorDisabled",
"colorError": "MuiSvgIcon-colorError",
"colorPrimary": "MuiSvgIcon-colorPrimary",
"colorSecondary": "MuiSvgIcon-colorSecondary",
"fontSizeInherit": "MuiSvgIcon-fontSizeInherit",
"fontSizeLarge": "MuiSvgIcon-fontSizeLarge",
"fontSizeSmall": "MuiSvgIcon-fontSizeSmall",
"root": "MuiSvgIcon-root",
}
}
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root MuiAvatar-fallback"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</ForwardRef(SvgIcon)>
</WithStyles(ForwardRef(SvgIcon))>
</ForwardRef>
</div>
</ForwardRef(Avatar)> </ForwardRef(Avatar)>
</WithStyles(ForwardRef(Avatar))> </WithStyles(ForwardRef(Avatar))>
</ForwardRef(Avatar)> </ForwardRef(Avatar)>
@@ -588,13 +818,35 @@ exports[`test Developers should render the component for maintainers with items
<div <div
aria-label="mgol" aria-label="mgol"
class="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault" class="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
/> >
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiAvatar-fallback"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</div>
</a> </a>
} }
className="MuiTooltip-popper" className="MuiTooltip-popper"
id={null} id={null}
open={false} open={false}
placement="bottom" placement="bottom"
popperOptions={
Object {
"modifiers": Object {
"arrow": Object {
"element": null,
"enabled": false,
},
},
}
}
transition={true} transition={true}
/> />
</ForwardRef(Tooltip)> </ForwardRef(Tooltip)>

View File

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

View File

@@ -10,8 +10,6 @@ import { StyledText, DistListItem, DistChips } from './styles';
const DistChip: FC<{ name: string }> = ({ name, children }) => const DistChip: FC<{ name: string }> = ({ name, children }) =>
children ? ( children ? (
<DistChips <DistChips
// lint rule conflicting with prettier
/* eslint-disable react/jsx-wrap-multilines */
label={ label={
<> <>
<b>{name}</b> <b>{name}</b>
@@ -19,7 +17,6 @@ const DistChip: FC<{ name: string }> = ({ name, children }) =>
{children} {children}
</> </>
} }
/* eslint-enable */
/> />
) : null; ) : null;

View File

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

View File

@@ -13,13 +13,12 @@ import node from './img/node.png';
const Engine: React.FC = () => { const Engine: React.FC = () => {
const { packageMeta } = useContext(DetailContext); const { packageMeta } = useContext(DetailContext);
const engines = packageMeta && packageMeta.latest && packageMeta.latest.engines; const engines = packageMeta?.latest?.engines;
if (!engines || (!engines.node && !engines.npm)) { if (!engines || (!engines.node && !engines.npm)) {
return null; return null;
} }
/* eslint-disable react/jsx-max-depth */
return ( return (
<Grid container={true}> <Grid container={true}>
{engines.node && ( {engines.node && (
@@ -45,7 +44,6 @@ const Engine: React.FC = () => {
)} )}
</Grid> </Grid>
); );
/* eslint-enable react/jsx-max-depth */
}; };
export default Engine; export default Engine;

View File

@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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\\">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>"`; 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\\"><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\\">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>"`;

View File

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

View File

@@ -1,6 +1,5 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { breakpoints } from '../../utils/styles/media';
import Icon from '../Icon/Icon'; import Icon from '../Icon/Icon';
import { Theme } from '../../design-tokens/theme'; import { Theme } from '../../design-tokens/theme';
@@ -12,29 +11,29 @@ export const Wrapper = styled('div')<{ theme?: Theme }>(props => ({
padding: '20px', padding: '20px',
})); }));
export const Inner = styled('div')({ export const Inner = styled('div')<{ theme?: Theme }>(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'flex-end', justifyContent: 'flex-end',
width: '100%', width: '100%',
[`@media (min-width: ${breakpoints.medium}px)`]: { [`@media (min-width: ${theme && theme.breakPoints.medium}px)`]: {
minWidth: 400, minWidth: 400,
maxWidth: 800, maxWidth: 800,
margin: 'auto', margin: 'auto',
justifyContent: 'space-between', justifyContent: 'space-between',
}, },
[`@media (min-width: ${breakpoints.large}px)`]: { [`@media (min-width: ${theme && theme.breakPoints.large}px)`]: {
maxWidth: 1240, maxWidth: 1240,
}, },
}); }));
export const Left = styled('div')({ export const Left = styled('div')<{ theme?: Theme }>(({ theme }) => ({
alignItems: 'center', alignItems: 'center',
display: 'none', display: 'none',
[`@media (min-width: ${breakpoints.medium}px)`]: { [`@media (min-width: ${theme && theme.breakPoints.medium}px)`]: {
display: 'flex', display: 'flex',
}, },
}); }));
export const Right = styled(Left)({ export const Right = styled(Left)({
display: 'flex', display: 'flex',

View File

@@ -1,16 +1,16 @@
import React from 'react'; import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { render, fireEvent, waitForElementToBeRemoved, waitForElement } from '../../utils/test-react-testing-library'; import { render, fireEvent, waitForElement, waitForElementToBeRemoved } from '../../utils/test-react-testing-library';
import { AppContextProvider } from '../../App';
import Header from './Header'; import Header from './Header';
const headerProps = { const props = {
username: 'verddacio-user', user: {
scope: 'test scope', username: 'verddacio-user',
withoutSearch: true, },
handleToggleLoginModal: jest.fn(), packages: [],
handleLogout: jest.fn(),
}; };
/* eslint-disable react/jsx-no-bind*/ /* eslint-disable react/jsx-no-bind*/
@@ -18,82 +18,70 @@ describe('<Header /> component with logged in state', () => {
test('should load the component in logged out state', () => { test('should load the component in logged out state', () => {
const { container, queryByTestId, getByText } = render( const { container, queryByTestId, getByText } = render(
<Router> <Router>
<Header <AppContextProvider packages={props.packages}>
onLogout={headerProps.handleLogout} <Header />
onToggleLoginModal={headerProps.handleToggleLoginModal} </AppContextProvider>
scope={headerProps.scope}
/>
</Router> </Router>
); );
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
expect(queryByTestId('header--menu-acountcircle')).toBeNull(); expect(queryByTestId('header--menu-accountcircle')).toBeNull();
expect(getByText('Login')).toBeTruthy(); expect(getByText('Login')).toBeTruthy();
}); });
test('should load the component in logged in state', () => { test('should load the component in logged in state', () => {
const { container, getByTestId, queryByText } = render( const { container, getByTestId, queryByText } = render(
<Router> <Router>
<Header <AppContextProvider packages={props.packages} user={props.user}>
onLogout={headerProps.handleLogout} <Header />
onToggleLoginModal={headerProps.handleToggleLoginModal} </AppContextProvider>
scope={headerProps.scope}
username={headerProps.username}
/>
</Router> </Router>
); );
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
expect(getByTestId('header--menu-acountcircle')).toBeTruthy(); expect(getByTestId('header--menu-accountcircle')).toBeTruthy();
expect(queryByText('Login')).toBeNull(); expect(queryByText('Login')).toBeNull();
}); });
test('should open login dialog', async () => { test('should open login dialog', async () => {
const { getByText } = render( const { getByText } = render(
<Router> <Router>
<Header <AppContextProvider packages={props.packages}>
onLogout={headerProps.handleLogout} <Header />
onToggleLoginModal={headerProps.handleToggleLoginModal} </AppContextProvider>
scope={headerProps.scope}
/>
</Router> </Router>
); );
const loginBtn = getByText('Login'); const loginBtn = getByText('Login');
fireEvent.click(loginBtn); fireEvent.click(loginBtn);
expect(headerProps.handleToggleLoginModal).toHaveBeenCalled(); const loginDialog = await waitForElement(() => getByText('Sign in'));
expect(loginDialog).toBeTruthy();
}); });
test('should logout the user', async () => { test('should logout the user', async () => {
const { getByText, getByTestId } = render( const { getByText, getByTestId } = render(
<Router> <Router>
<Header <AppContextProvider packages={props.packages} user={props.user}>
onLogout={headerProps.handleLogout} <Header />
onToggleLoginModal={headerProps.handleToggleLoginModal} </AppContextProvider>
scope={headerProps.scope}
username={headerProps.username}
/>
</Router> </Router>
); );
const headerMenuAccountCircle = getByTestId('header--menu-acountcircle'); const headerMenuAccountCircle = getByTestId('header--menu-accountcircle');
fireEvent.click(headerMenuAccountCircle); fireEvent.click(headerMenuAccountCircle);
// wait for button Logout's appearance and return the element // wait for button Logout's appearance and return the element
const logoutBtn = await waitForElement(() => getByText('Logout')); const logoutBtn = await waitForElement(() => getByText('Logout'));
fireEvent.click(logoutBtn); fireEvent.click(logoutBtn);
expect(headerProps.handleLogout).toHaveBeenCalled(); expect(getByText('Login')).toBeTruthy();
}); });
test("The question icon should open a new tab of verdaccio's website - installation doc", async () => { test("The question icon should open a new tab of verdaccio's website - installation doc", () => {
const { getByTestId } = render( const { getByTestId } = render(
<Router> <Router>
<Header <AppContextProvider packages={props.packages} user={props.user}>
onLogout={headerProps.handleLogout} <Header />
onToggleLoginModal={headerProps.handleToggleLoginModal} </AppContextProvider>
scope={headerProps.scope}
username={headerProps.username}
/>
</Router> </Router>
); );
@@ -104,12 +92,9 @@ describe('<Header /> component with logged in state', () => {
test('should open the registrationInfo modal when clicking on the info icon', async () => { test('should open the registrationInfo modal when clicking on the info icon', async () => {
const { getByTestId } = render( const { getByTestId } = render(
<Router> <Router>
<Header <AppContextProvider packages={props.packages} user={props.user}>
onLogout={headerProps.handleLogout} <Header />
onToggleLoginModal={headerProps.handleToggleLoginModal} </AppContextProvider>
scope={headerProps.scope}
username={headerProps.username}
/>
</Router> </Router>
); );
@@ -124,12 +109,9 @@ describe('<Header /> component with logged in state', () => {
test('should close the registrationInfo modal when clicking on the button close', async () => { test('should close the registrationInfo modal when clicking on the button close', async () => {
const { getByTestId, getByText, queryByTestId } = render( const { getByTestId, getByText, queryByTestId } = render(
<Router> <Router>
<Header <AppContextProvider packages={props.packages} user={props.user}>
onLogout={headerProps.handleLogout} <Header />
onToggleLoginModal={headerProps.handleToggleLoginModal} </AppContextProvider>
scope={headerProps.scope}
username={headerProps.username}
/>
</Router> </Router>
); );
@@ -144,6 +126,6 @@ describe('<Header /> component with logged in state', () => {
queryByTestId('registryInfo--dialog') queryByTestId('registryInfo--dialog')
); );
expect(hasRegistrationInfoModalBeenRemoved).toBeTruthy(); 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,8 +1,11 @@
import React, { useState } from 'react'; import React, { useState, useContext } from 'react';
import Search from '../Search'; import storage from '../../utils/storage';
import { getRegistryURL } from '../../utils/url'; import { getRegistryURL } from '../../utils/url';
import Button from '../../muiComponents/Button'; 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 { NavBar, InnerNavBar, MobileNavBar, InnerMobileNavBar } from './styles';
import HeaderLeft from './HeaderLeft'; import HeaderLeft from './HeaderLeft';
@@ -10,31 +13,44 @@ import HeaderRight from './HeaderRight';
import HeaderInfoDialog from './HeaderInfoDialog'; import HeaderInfoDialog from './HeaderInfoDialog';
interface Props { interface Props {
logo?: string;
username?: string;
onLogout: () => void;
onToggleLoginModal: () => void;
scope: string;
withoutSearch?: boolean; withoutSearch?: boolean;
} }
/* eslint-disable react/jsx-max-depth */
/* eslint-disable react/jsx-no-bind*/ /* eslint-disable react/jsx-no-bind*/
const Header: React.FC<Props> = ({ logo, withoutSearch, username, onLogout, onToggleLoginModal, scope }) => { const Header: React.FC<Props> = ({ withoutSearch }) => {
const appContext = useContext(AppContext);
const [isInfoDialogOpen, setOpenInfoDialog] = useState(); const [isInfoDialogOpen, setOpenInfoDialog] = useState();
const [showMobileNavBar, setShowMobileNavBar] = useState(); const [showMobileNavBar, setShowMobileNavBar] = useState();
const [showLoginModal, setShowLoginModal] = useState(false);
if (!appContext) {
throw Error('The app Context was not correct used');
}
const { user, scope, setUser } = appContext;
const logo = window.VERDACCIO_LOGO;
/**
* Logouts user
* Required by: <Header />
*/
const handleLogout = () => {
storage.removeItem('username');
storage.removeItem('token');
setUser(undefined);
};
return ( return (
<> <>
<NavBar position="static"> <NavBar data-testid="header" position="static">
<InnerNavBar> <InnerNavBar>
<HeaderLeft logo={logo} /> <HeaderLeft logo={logo} />
<HeaderRight <HeaderRight
onLogout={onLogout} onLogout={handleLogout}
onOpenRegistryInfoDialog={() => setOpenInfoDialog(true)} onOpenRegistryInfoDialog={() => setOpenInfoDialog(true)}
onToggleLogin={onToggleLoginModal} onToggleLogin={() => setShowLoginModal(!showLoginModal)}
onToggleMobileNav={() => setShowMobileNavBar(!showMobileNavBar)} onToggleMobileNav={() => setShowMobileNavBar(!showMobileNavBar)}
username={username} username={user && user.username}
withoutSearch={withoutSearch} withoutSearch={withoutSearch}
/> />
</InnerNavBar> </InnerNavBar>
@@ -55,6 +71,7 @@ const Header: React.FC<Props> = ({ logo, withoutSearch, username, onLogout, onTo
</Button> </Button>
</MobileNavBar> </MobileNavBar>
)} )}
{!user && <LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />}
</> </>
); );
}; };

View File

@@ -11,7 +11,7 @@ interface Props {
const HeaderGreetings: React.FC<Props> = ({ username }) => ( const HeaderGreetings: React.FC<Props> = ({ username }) => (
<> <>
<Greetings>{'Hi,'}</Greetings> <Greetings>{'Hi,'}</Greetings>
<Label capitalize={true} text={username} weight="bold" /> <Label capitalize={true} data-testid="greetings-label" text={username} weight="bold" />
</> </>
); );

View File

@@ -16,7 +16,6 @@ interface Props {
onLoggedInMenuClose: () => void; onLoggedInMenuClose: () => void;
} }
/* eslint-disable react/jsx-max-depth */
const HeaderMenu: React.FC<Props> = ({ const HeaderMenu: React.FC<Props> = ({
onLogout, onLogout,
username, username,
@@ -28,7 +27,7 @@ const HeaderMenu: React.FC<Props> = ({
<> <>
<IconButton <IconButton
color="inherit" color="inherit"
data-testid="header--menu-acountcircle" data-testid="header--menu-accountcircle"
id="header--button-account" id="header--button-account"
onClick={onLoggedInMenu}> onClick={onLoggedInMenu}>
<AccountCircle /> <AccountCircle />
@@ -48,7 +47,7 @@ const HeaderMenu: React.FC<Props> = ({
<MenuItem disabled={true}> <MenuItem disabled={true}>
<HeaderGreetings username={username} /> <HeaderGreetings username={username} />
</MenuItem> </MenuItem>
<MenuItem button={true} id="header--button-logout" onClick={onLogout}> <MenuItem button={true} data-testid="header--button-logout" id="header--button-logout" onClick={onLogout}>
{'Logout'} {'Logout'}
</MenuItem> </MenuItem>
</Menu> </Menu>

View File

@@ -53,7 +53,7 @@ const HeaderRight: React.FC<Props> = ({
}; };
return ( return (
<RightSide> <RightSide data-testid="header-right">
{!withoutSearch && ( {!withoutSearch && (
<HeaderToolTip onClick={onToggleMobileNav} title={'Search packages'} tooltipIconType={'search'} /> <HeaderToolTip onClick={onToggleMobileNav} title={'Search packages'} tooltipIconType={'search'} />
)} )}

View File

@@ -141,6 +141,7 @@ exports[`<Header /> component with logged in state should load the component in
<header <header
class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-24 emotion-25 MuiAppBar-colorPrimary" class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-24 emotion-25 MuiAppBar-colorPrimary"
data-testid="header"
> >
<div <div
class="MuiToolbar-root MuiToolbar-regular emotion-22 emotion-23 MuiToolbar-gutters" class="MuiToolbar-root MuiToolbar-regular emotion-22 emotion-23 MuiToolbar-gutters"
@@ -213,6 +214,7 @@ exports[`<Header /> component with logged in state should load the component in
</div> </div>
<div <div
class="MuiToolbar-root MuiToolbar-regular emotion-20 emotion-21 MuiToolbar-gutters" class="MuiToolbar-root MuiToolbar-regular emotion-20 emotion-21 MuiToolbar-gutters"
data-testid="header-right"
> >
<button <button
class="MuiButtonBase-root MuiIconButton-root emotion-16 emotion-17 MuiIconButton-colorInherit" class="MuiButtonBase-root MuiIconButton-root emotion-16 emotion-17 MuiIconButton-colorInherit"
@@ -302,7 +304,7 @@ exports[`<Header /> component with logged in state should load the component in
</button> </button>
<button <button
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit" class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorInherit"
data-testid="header--menu-acountcircle" data-testid="header--menu-accountcircle"
id="header--button-account" id="header--button-account"
tabindex="0" tabindex="0"
type="button" type="button"
@@ -472,6 +474,7 @@ exports[`<Header /> component with logged in state should load the component in
<header <header
class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-24 emotion-25 MuiAppBar-colorPrimary" class="MuiPaper-root MuiPaper-elevation4 MuiAppBar-root MuiAppBar-positionStatic emotion-24 emotion-25 MuiAppBar-colorPrimary"
data-testid="header"
> >
<div <div
class="MuiToolbar-root MuiToolbar-regular emotion-22 emotion-23 MuiToolbar-gutters" class="MuiToolbar-root MuiToolbar-regular emotion-22 emotion-23 MuiToolbar-gutters"
@@ -544,6 +547,7 @@ exports[`<Header /> component with logged in state should load the component in
</div> </div>
<div <div
class="MuiToolbar-root MuiToolbar-regular emotion-20 emotion-21 MuiToolbar-gutters" class="MuiToolbar-root MuiToolbar-regular emotion-20 emotion-21 MuiToolbar-gutters"
data-testid="header-right"
> >
<button <button
class="MuiButtonBase-root MuiIconButton-root emotion-16 emotion-17 MuiIconButton-colorInherit" class="MuiButtonBase-root MuiIconButton-root emotion-16 emotion-17 MuiIconButton-colorInherit"

View File

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

View File

@@ -2,16 +2,16 @@ import React, { useContext } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { DetailContext } from '../../pages/Version'; import { DetailContext } from '../../pages/Version';
import { fontWeight } from '../../utils/styles/sizes';
import Text from '../../muiComponents/Text'; import Text from '../../muiComponents/Text';
import List from '../../muiComponents/List'; import List from '../../muiComponents/List';
import { Theme } from '../../design-tokens/theme';
import InstallListItem, { DependencyManager } from './InstallListItem'; import InstallListItem, { DependencyManager } from './InstallListItem';
const StyledText = styled(Text)({ const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: fontWeight.bold, fontWeight: props.theme && props.theme.fontWeight.bold,
textTransform: 'capitalize', textTransform: 'capitalize',
}); }));
const Install: React.FC = () => { const Install: React.FC = () => {
const detailContext = useContext(DetailContext); const detailContext = useContext(DetailContext);

View File

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

View File

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

View File

@@ -1,127 +0,0 @@
/**
* @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

@@ -1,248 +0,0 @@
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

@@ -1,5 +0,0 @@
// 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

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

View File

@@ -1,23 +0,0 @@
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

@@ -0,0 +1,106 @@
import React from 'react';
import { render, waitForElement, fireEvent, waitForElementToBeRemoved } from '../../utils/test-react-testing-library';
import AppContext, { AppContextProps } from '../../App/AppContext';
import api from '../../utils/api';
import LoginDialog from './LoginDialog';
const appContextValue: AppContextProps = {
scope: '',
packages: [],
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 { getByText } = render(
<AppContext.Provider value={appContextValue}>
<LoginDialog onClose={props.onClose} open={props.open} />
</AppContext.Provider>
);
const loginDialogHeading = await waitForElement(() => getByText('Sign in'));
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');
});

View File

@@ -0,0 +1,56 @@
import React, { useState, useContext, useCallback } from 'react';
import { makeLogin } from '../../utils/login';
import storage from '../../utils/storage';
import Dialog from '../../muiComponents/Dialog';
import DialogContent from '../../muiComponents/DialogContent';
import AppContext from '../../App/AppContext';
import LoginDialogCloseButton from './LoginDialogCloseButton';
import LoginDialogForm, { FormValues } from './LoginDialogForm';
import LoginDialogHeader from './LoginDialogHeader';
interface Props {
open?: boolean;
onClose: () => void;
}
const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
const appContext = useContext(AppContext);
if (!appContext) {
throw Error('The app Context was not correct used');
}
const [error, setError] = useState();
const handleDoLogin = useCallback(
async (data: FormValues) => {
const { username, token, error } = await makeLogin(data.username, data.password);
if (error) {
setError(error);
}
if (username && token) {
storage.setItem('username', username);
storage.setItem('token', token);
appContext.setUser({ username });
onClose();
}
},
[appContext, onClose]
);
return (
<Dialog fullWidth={true} id="login--dialog" maxWidth="sm" onClose={onClose} open={open}>
<LoginDialogCloseButton onClose={onClose} />
<DialogContent>
<LoginDialogHeader />
<LoginDialogForm error={error} onSubmit={handleDoLogin} />
</DialogContent>
</Dialog>
);
};
export default LoginDialog;

View File

@@ -0,0 +1,28 @@
import React from 'react';
import styled from '@emotion/styled';
import CloseIcon from '@material-ui/icons/Close';
import DialogTitle from '../../muiComponents/DialogTitle';
import IconButton from '../../muiComponents/IconButton';
import { Theme } from '../../design-tokens/theme';
const StyledIconButton = styled(IconButton)<{ theme?: Theme }>(({ theme }) => ({
position: 'absolute',
right: theme.spacing() / 2,
top: theme.spacing() / 2,
color: theme.palette.grey[500],
}));
interface Props {
onClose: () => void;
}
const LoginDialogCloseButton: React.FC<Props> = ({ onClose }) => (
<DialogTitle>
<StyledIconButton data-testid="close-login-dialog-button" onClick={onClose}>
<CloseIcon titleAccess="Close Dialog" />
</StyledIconButton>
</DialogTitle>
);
export default LoginDialogCloseButton;

View File

@@ -0,0 +1,94 @@
import React, { memo } from 'react';
import styled from '@emotion/styled';
import useForm from 'react-hook-form/dist/react-hook-form.ie11';
import TextField from '../../muiComponents/TextField';
import Button from '../../muiComponents/Button';
import { Theme } from '../../design-tokens/theme';
import { LoginError } from '../../utils/login';
import LoginDialogFormError from './LoginDialogFormError';
const StyledForm = styled('form')<{ theme?: Theme }>(({ theme }) => ({
marginTop: theme.spacing(1),
}));
const StyledButton = styled(Button)<{ theme?: Theme }>(({ theme }) => ({
margin: theme.spacing(3, 0, 2),
}));
export interface FormValues {
username: string;
password: string;
}
interface Props {
onSubmit: (formValues: FormValues) => void;
error?: LoginError;
}
const LoginDialogForm = memo(({ onSubmit, error }: Props) => {
const {
register,
errors,
handleSubmit,
formState: { isValid },
} = useForm<FormValues>({ mode: 'onChange' });
const onSubmitForm = (formValues: FormValues) => {
onSubmit(formValues);
};
return (
<StyledForm noValidate={true} onSubmit={handleSubmit(onSubmitForm)}>
<TextField
autoComplete="username"
error={!!errors.username}
fullWidth={true}
helperText={errors.username?.message}
id="login--dialog-username"
inputRef={register({
required: { value: true, message: 'This field is required' },
minLength: { value: 2, message: 'This field required the min length of 2' },
})}
label="Username"
margin="normal"
name="username"
placeholder="Your username"
required={true}
variant="outlined"
/>
<TextField
autoComplete="current-password"
error={!!errors.password}
fullWidth={true}
helperText={errors.password?.message}
id="login--dialog-password"
inputRef={register({
required: { value: true, message: 'This field is required' },
minLength: { value: 2, message: 'This field required the min length of 2' },
})}
label="Password"
margin="normal"
name="password"
placeholder="Your strong password"
required={true}
type="password"
variant="outlined"
/>
{error && <LoginDialogFormError error={error} />}
<StyledButton
color="primary"
disabled={!isValid}
fullWidth={true}
id="login--dialog-button-submit"
size="large"
type="submit"
variant="contained">
{'Sign In'}
</StyledButton>
</StyledForm>
);
});
export default LoginDialogForm;

View File

@@ -0,0 +1,42 @@
import React, { memo } from 'react';
import styled from '@emotion/styled';
import Error from '@material-ui/icons/Error';
import SnackbarContent from '../../muiComponents/SnackbarContent';
import Box from '../../muiComponents/Box';
import { Theme } from '../../design-tokens/theme';
import { LoginError } from '../../utils/login';
const StyledSnackbarContent = styled(SnackbarContent)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor: theme.palette.error.dark,
}));
const StyledErrorIcon = styled(Error)<{ theme?: Theme }>(({ theme }) => ({
fontSize: 20,
opacity: 0.9,
marginRight: theme.spacing(1),
}));
export interface FormValues {
username: string;
password: string;
}
interface Props {
error: LoginError;
}
const LoginDialogFormError = memo(({ error }: Props) => {
return (
<StyledSnackbarContent
message={
<Box alignItems="center" display="flex">
<StyledErrorIcon />
{error.description}
</Box>
}
/>
);
});
export default LoginDialogFormError;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import styled from '@emotion/styled';
import LockOutlined from '@material-ui/icons/LockOutlined';
import CloseIcon from '@material-ui/icons/Close';
import Heading from '../../muiComponents/Heading';
import Avatar from '../../muiComponents/Avatar';
import Box from '../../muiComponents/Box';
import IconButton from '../../muiComponents/IconButton';
import { Theme } from '../../design-tokens/theme';
const StyledAvatar = styled(Avatar)<{ theme?: Theme }>(({ theme }) => ({
margin: theme.spacing(1),
backgroundColor: theme.palette.primary.main,
}));
const StyledIconButton = styled(IconButton)<{ theme?: Theme }>(({ theme }) => ({
position: 'absolute',
right: theme.spacing() / 2,
top: theme.spacing() / 2,
color: theme.palette.grey[500],
}));
interface Props {
onClose?: () => void;
}
const LoginDialogHeader: React.FC<Props> = ({ onClose }) => {
return (
<Box alignItems="center" display="flex" flexDirection="column" position="relative">
{onClose && (
<StyledIconButton aria-label="Close" onClick={onClose}>
<CloseIcon />
</StyledIconButton>
)}
<StyledAvatar>
<LockOutlined />
</StyledAvatar>
<Heading>{'Sign in'}</Heading>
</Box>
);
};
export default LoginDialogHeader;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LoginDialog /> component should render the component in default state 1`] = `null`;

View File

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

View File

@@ -5,7 +5,6 @@ import { useHistory } from 'react-router-dom';
import Box from '../../muiComponents/Box'; import Box from '../../muiComponents/Box';
import Button from '../../muiComponents/Button'; import Button from '../../muiComponents/Button';
import Heading from '../../muiComponents/Heading'; import Heading from '../../muiComponents/Heading';
import { spacings } from '../../utils/styles/spacings';
import { Theme } from '../../design-tokens/theme'; import { Theme } from '../../design-tokens/theme';
import PackageImg from './img/package.svg'; import PackageImg from './img/package.svg';
@@ -21,7 +20,7 @@ const EmptyPackage = styled('img')({
const StyledHeading = styled(Heading)<{ theme?: Theme }>(props => ({ const StyledHeading = styled(Heading)<{ theme?: Theme }>(props => ({
color: props.theme && props.theme.palette.primary.main, color: props.theme && props.theme.palette.primary.main,
marginBottom: spacings.sm, marginBottom: 16,
})); }));
const NotFound: React.FC = () => { const NotFound: React.FC = () => {

View File

@@ -9,7 +9,7 @@ import fileSizeSI from '../../utils/file-size';
import { formatDate, formatDateDistance } from '../../utils/package'; import { formatDate, formatDateDistance } from '../../utils/package';
import Tooltip from '../../muiComponents/Tooltip'; import Tooltip from '../../muiComponents/Tooltip';
import { isURL } from '../../utils/url'; import { isURL } from '../../utils/url';
import { downloadHandler } from '../ActionBar/ActionBar'; import { downloadTarball } from '../ActionBar';
import ListItem from '../../muiComponents/ListItem'; import ListItem from '../../muiComponents/ListItem';
import Grid from '../../muiComponents/Grid'; import Grid from '../../muiComponents/Grid';
@@ -104,7 +104,7 @@ const Package: React.FC<PackageInterface> = ({
<OverviewItem> <OverviewItem>
<Icon name="time" /> <Icon name="time" />
<Published>{`Published on ${formatDate(time)} •`}</Published> <Published>{`Published on ${formatDate(time)} •`}</Published>
{`${formatDateDistance(time)} ago`} {formatDateDistance(time)}
</OverviewItem> </OverviewItem>
); );
@@ -114,7 +114,6 @@ const Package: React.FC<PackageInterface> = ({
<a href={homepage} target={'_blank'}> <a href={homepage} target={'_blank'}>
<Tooltip aria-label={'Homepage'} title={'Visit homepage'}> <Tooltip aria-label={'Homepage'} title={'Visit homepage'}>
<IconButton aria-label={'Homepage'}> <IconButton aria-label={'Homepage'}>
{/* eslint-disable-next-line react/jsx-max-depth */}
<HomeIcon /> <HomeIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -128,7 +127,6 @@ const Package: React.FC<PackageInterface> = ({
<a href={bugs.url} target={'_blank'}> <a href={bugs.url} target={'_blank'}>
<Tooltip aria-label={'Bugs'} title={'Open an issue'}> <Tooltip aria-label={'Bugs'} title={'Open an issue'}>
<IconButton aria-label={'Bugs'}> <IconButton aria-label={'Bugs'}>
{/* eslint-disable-next-line react/jsx-max-depth */}
<BugReport /> <BugReport />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -140,10 +138,9 @@ const Package: React.FC<PackageInterface> = ({
dist.tarball && dist.tarball &&
isURL(dist.tarball) && ( isURL(dist.tarball) && (
// eslint-disable-next-line // eslint-disable-next-line
<a onClick={() => downloadHandler(dist.tarball.replace(`https://registry.npmjs.org/`, window.location.href))} target={'_blank'}> <a onClick={downloadTarball(dist.tarball.replace(`https://registry.npmjs.org/`, window.location.href))} target={'_blank'}>
<Tooltip aria-label={'Download the tar file'} title={'Download tarball'}> <Tooltip aria-label={'Download the tar file'} title={'Download tarball'}>
<IconButton aria-label={'Download'}> <IconButton aria-label={'Download'}>
{/* eslint-disable-next-line react/jsx-max-depth */}
<DownloadIcon /> <DownloadIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -155,7 +152,6 @@ const Package: React.FC<PackageInterface> = ({
<Grid container={true} item={true} xs={12}> <Grid container={true} item={true} xs={12}>
<Grid item={true} xs={true}> <Grid item={true} xs={true}>
<WrapperLink to={`/-/web/detail/${packageName}`}> <WrapperLink to={`/-/web/detail/${packageName}`}>
{/* eslint-disable-next-line react/jsx-max-depth */}
<PackageTitle className="package-title">{packageName}</PackageTitle> <PackageTitle className="package-title">{packageName}</PackageTitle>
</WrapperLink> </WrapperLink>
</Grid> </Grid>

View File

@@ -1,10 +1,8 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { breakpoints } from '../../utils/styles/media';
import Ico from '../Icon'; import Ico from '../Icon';
import Label from '../Label'; import Label from '../Label';
import { fontWeight } from '../../utils/styles/sizes';
import { default as MuiIconButton } from '../../muiComponents/IconButton'; import { default as MuiIconButton } from '../../muiComponents/IconButton';
import { default as Photo } from '../../muiComponents/Avatar'; import { default as Photo } from '../../muiComponents/Avatar';
import List from '../../muiComponents/List'; import List from '../../muiComponents/List';
@@ -13,18 +11,18 @@ import Grid from '../../muiComponents/Grid';
import ListItemText from '../../muiComponents/ListItemText'; import ListItemText from '../../muiComponents/ListItemText';
import { Theme } from '../../design-tokens/theme'; import { Theme } from '../../design-tokens/theme';
export const OverviewItem = styled('span')<{ theme?: Theme }>(props => ({ export const OverviewItem = styled('span')<{ theme?: Theme }>(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
margin: '0 0 0 16px', margin: '0 0 0 16px',
color: props.theme && props.theme.palette.greyLight2, color: theme && theme.palette.greyLight2,
fontSize: 12, fontSize: 12,
[`@media (max-width: ${breakpoints.medium}px)`]: { [`@media (max-width: ${theme && theme.breakPoints.medium}px)`]: {
':nth-of-type(3)': { ':nth-of-type(3)': {
display: 'none', display: 'none',
}, },
}, },
[`@media (max-width: ${breakpoints.small}px)`]: { [`@media (max-width: ${theme && theme.breakPoints.small}px)`]: {
':nth-of-type(4)': { ':nth-of-type(4)': {
display: 'none', display: 'none',
}, },
@@ -43,7 +41,7 @@ export const Published = styled('span')<{ theme?: Theme }>(props => ({
export const Text = styled(Label)<{ theme?: Theme }>(props => ({ export const Text = styled(Label)<{ theme?: Theme }>(props => ({
fontSize: '12px', fontSize: '12px',
fontWeight: fontWeight.semiBold, fontWeight: props.theme && props.theme.fontWeight.semiBold,
color: props.theme && props.theme.palette.greyLight2, color: props.theme && props.theme.palette.greyLight2,
})); }));
@@ -68,17 +66,17 @@ export const WrapperLink = styled(Link)({
textDecoration: 'none', textDecoration: 'none',
}); });
export const PackageTitle = styled('span')<{ theme?: Theme }>(props => ({ export const PackageTitle = styled('span')<{ theme?: Theme }>(({ theme }) => ({
fontWeight: 600, fontWeight: theme && theme.fontWeight.bold,
fontSize: 20, fontSize: 20,
display: 'block', display: 'block',
marginBottom: 12, marginBottom: 12,
color: props.theme && props.theme.palette.eclipse, color: theme && theme.palette.eclipse,
cursor: 'pointer', cursor: 'pointer',
':hover': { ':hover': {
color: props.theme && props.theme.palette.black, color: theme && theme.palette.black,
}, },
[`@media (max-width: ${breakpoints.small}px)`]: { [`@media (max-width: ${theme && theme.breakPoints.small}px)`]: {
fontSize: 14, fontSize: 14,
marginBottom: 8, marginBottom: 8,
}, },
@@ -105,14 +103,14 @@ export const IconButton = styled(MuiIconButton)({
}, },
}); });
export const TagContainer = styled('span')({ export const TagContainer = styled('span')<{ theme?: Theme }>(({ theme }) => ({
marginTop: 8, marginTop: 8,
marginBottom: 12, marginBottom: 12,
display: 'block', display: 'block',
[`@media (max-width: ${breakpoints.medium}px)`]: { [`@media (max-width: ${theme && theme.breakPoints.medium}px)`]: {
display: 'none', display: 'none',
}, },
}); }));
export const PackageListItemText = styled(ListItemText)({ export const PackageListItemText = styled(ListItemText)({
paddingRight: 0, paddingRight: 0,

View File

@@ -1,4 +1,5 @@
import React, { Fragment, ReactNode } from 'react'; import React, { Fragment, ReactNode } from 'react';
import styled from '@emotion/styled';
import Package from '../Package'; import Package from '../Package';
import Help from '../Help'; import Help from '../Help';
@@ -6,7 +7,10 @@ import { formatLicense } from '../../utils/package';
import { PackageInterface } from '../Package/Package'; import { PackageInterface } from '../Package/Package';
import Divider from '../../muiComponents/Divider'; import Divider from '../../muiComponents/Divider';
import * as classes from './styles'; const PkgContainer = styled('div')({
margin: 0,
padding: 0,
});
interface Props { interface Props {
packages: PackageInterface[]; packages: PackageInterface[];
@@ -32,7 +36,7 @@ export const PackageList: React.FC<Props> = ({ packages }) => {
return ( return (
<div className={'package-list-items'}> <div className={'package-list-items'}>
<div className={classes.pkgContainer}>{hasPackages() ? renderPackages() : <Help />}</div> <PkgContainer>{hasPackages() ? renderPackages() : <Help />}</PkgContainer>
</div> </div>
); );
}; };

View File

@@ -1,18 +0,0 @@
import { css } from 'emotion';
import { fontWeight, fontSize } from '../../utils/styles/sizes';
export const listTitle = css({
fontWeight: fontWeight.regular,
fontSize: fontSize.xl,
margin: `0 0 10px 0`,
});
export const pkgContainer = css`
margin: 0;
padding: 0;
& .listTitle {
${listTitle}
}
`;

View File

@@ -1,6 +1,5 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { fontSize } from '../../utils/styles/sizes';
import DialogTitle from '../../muiComponents/DialogTitle'; import DialogTitle from '../../muiComponents/DialogTitle';
import DialogContent from '../../muiComponents/DialogContent'; import DialogContent from '../../muiComponents/DialogContent';
import { Theme } from '../../design-tokens/theme'; import { Theme } from '../../design-tokens/theme';
@@ -8,7 +7,7 @@ import { Theme } from '../../design-tokens/theme';
export const Title = styled(DialogTitle)<{ theme?: Theme }>(props => ({ export const Title = styled(DialogTitle)<{ theme?: Theme }>(props => ({
backgroundColor: props.theme && props.theme.palette.primary.main, backgroundColor: props.theme && props.theme.palette.primary.main,
color: props.theme && props.theme.palette.white, color: props.theme && props.theme.palette.white,
fontSize: fontSize.lg, fontSize: props.theme && props.theme.fontSize.lg,
})); }));
export const Content = styled(DialogContent)({ export const Content = styled(DialogContent)({

View File

@@ -1,64 +1,58 @@
import React from 'react'; import React from 'react';
import { mount } from '../../utils/test-enzyme'; import { render } from '../../utils/test-react-testing-library';
import { DetailContext, DetailContextProps } from '../../pages/Version';
import Repository from './Repository'; import Repository from './Repository';
import data from './__partials__/data.json';
jest.mock('./img/git.png', () => ''); const detailContextValue: DetailContextProps = {
packageName: 'foo',
readMe: 'readMe',
enableLoading: () => {},
isLoading: false,
hasNotBeenFound: false,
packageMeta: data,
};
const mockPackageMeta: jest.Mock = jest.fn(() => ({ const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({ contextValue }) => (
latest: { <DetailContext.Provider value={contextValue}>
homepage: 'https://verdaccio.tld', <Repository />
bugs: { </DetailContext.Provider>
url: 'https://verdaccio.tld/bugs', );
},
dist: {
tarball: 'https://verdaccio.tld/download',
},
},
}));
jest.mock('../../pages/Version', () => ({
DetailContextConsumer: component => {
return component.children({ packageMeta: mockPackageMeta() });
},
}));
describe('<Repository /> component', () => { describe('<Repository /> component', () => {
beforeEach(() => { test('should load the component in default state', () => {
jest.resetAllMocks(); const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
}); expect(container.firstChild).toMatchSnapshot();
test('should render the component in default state', () => {
const packageMeta = {
latest: {
repository: {
type: 'git',
url: 'git+https://github.com/verdaccio/ui.git',
},
},
};
mockPackageMeta.mockImplementation(() => packageMeta);
const wrapper = mount(<Repository />);
expect(wrapper.html()).toMatchSnapshot();
}); });
test('should render the component in with no repository data', () => { test('should render the component in with no repository data', () => {
const packageMeta = { const packageMeta = {
latest: {}, ...detailContextValue.packageMeta,
latest: {
...detailContextValue.packageMeta?.latest,
repository: undefined,
},
}; };
mockPackageMeta.mockImplementation(() => packageMeta); const { queryByText } = render(
<ComponentToBeRendered
contextValue={{
...detailContextValue,
packageMeta,
}}
/>
);
const wrapper = mount(<Repository />); expect(queryByText('Repository')).toBeFalsy();
expect(wrapper.html()).toEqual('');
}); });
test('should render the component in with invalid url', () => { test('should render the component in with invalid url', () => {
const packageMeta = { const packageMeta = {
...detailContextValue.packageMeta,
latest: { latest: {
...detailContextValue.packageMeta?.latest,
repository: { repository: {
type: 'git', type: 'git',
url: 'git://github.com/verdaccio/ui.git', url: 'git://github.com/verdaccio/ui.git',
@@ -66,9 +60,15 @@ describe('<Repository /> component', () => {
}, },
}; };
mockPackageMeta.mockImplementation(() => packageMeta); const { queryByText } = render(
<ComponentToBeRendered
contextValue={{
...detailContextValue,
packageMeta,
}}
/>
);
const wrapper = mount(<Repository />); expect(queryByText('Repository')).toBeFalsy();
expect(wrapper.html()).toEqual('');
}); });
}); });

View File

@@ -1,57 +1,76 @@
/* eslint react/jsx-max-depth: 0 */ import React from 'react';
import styled from '@emotion/styled';
import React, { Component, Fragment, ReactElement } from 'react';
import Avatar from '../../muiComponents/Avatar'; import Avatar from '../../muiComponents/Avatar';
import { DetailContextConsumer } from '../../pages/Version'; import Text from '../../muiComponents/Text';
import ListItem from '../../muiComponents/ListItem';
import ListItemText from '../../muiComponents/ListItemText';
import { isURL } from '../../utils/url'; import { isURL } from '../../utils/url';
import CopyToClipBoard from '../CopyToClipBoard'; import CopyToClipBoard from '../CopyToClipBoard';
import List from '../../muiComponents/List'; import List from '../../muiComponents/List';
import { DetailContext } from '../../pages/Version';
import { Theme } from '../../design-tokens/theme';
import git from './img/git.png'; import git from './img/git.png';
import { GithubLink, StyledText, RepositoryListItem, RepositoryListItemText } from './styles';
class Repository extends Component { const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
public render(): ReactElement<HTMLElement> { fontWeight: props.theme && props.theme.fontWeight.bold,
return ( textTransform: 'capitalize',
<DetailContextConsumer> }));
{context => {
return context && context.packageMeta && this.renderRepository(context.packageMeta); const GithubLink = styled('a')<{ theme?: Theme }>(props => ({
}} color: props.theme && props.theme.palette.primary.main,
</DetailContextConsumer> }));
);
const RepositoryListItem = styled(ListItem)({
padding: 0,
':hover': {
backgroundColor: 'transparent',
},
});
const RepositoryListItemText = styled(ListItemText)({
padding: '0 10px',
margin: 0,
});
const Repository: React.FC = () => {
const detailContext = React.useContext(DetailContext);
const { packageMeta } = detailContext;
if (!packageMeta?.latest?.repository?.url || !isURL(packageMeta.latest.repository.url)) {
return null;
} }
private renderRepositoryText(url: string): ReactElement<HTMLElement> { const { url } = packageMeta.latest.repository;
return (
<GithubLink href={url} target="_blank">
{url}
</GithubLink>
);
}
private renderRepository = packageMeta => { const getCorrectRepositoryURL = (): string => {
const { repository: { url = null } = {} } = packageMeta.latest; if (!url.includes('git+')) {
return url;
if (!url || isURL(url) === false) {
return null;
} }
return ( return url.split('git+')[1];
<Fragment>
<List dense={true} subheader={<StyledText variant="subtitle1">{'Repository'}</StyledText>}>
<RepositoryListItem button={true}>
<Avatar src={git} />
<RepositoryListItemText primary={this.renderContent(url)} />
</RepositoryListItem>
</List>
</Fragment>
);
}; };
private renderContent(url: string): ReactElement<HTMLElement> { const repositoryURL = getCorrectRepositoryURL();
return <CopyToClipBoard text={url}>{this.renderRepositoryText(url)}</CopyToClipBoard>;
} return (
} <List dense={true} subheader={<StyledText variant="subtitle1">{'Repository'}</StyledText>}>
<RepositoryListItem button={true}>
<Avatar src={git} />
<RepositoryListItemText
primary={
<CopyToClipBoard text={repositoryURL}>
<GithubLink href={repositoryURL} target="_blank">
{repositoryURL}
</GithubLink>
</CopyToClipBoard>
}
/>
</RepositoryListItem>
</List>
);
};
export default Repository; export default Repository;

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Repository /> component should render the component in default state 1`] = `"<ul class=\\"MuiList-root MuiList-dense MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText e1wmjxnh0 MuiTypography-subtitle1\\">Repository</h6><div class=\\"MuiButtonBase-root MuiListItem-root css-719o4l-RepositoryListItem e1wmjxnh4 MuiListItem-dense MuiListItem-gutters MuiListItem-button\\" tabindex=\\"0\\" role=\\"button\\" aria-disabled=\\"false\\"><div class=\\"MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault\\"></div><div class=\\"MuiListItemText-root css-1lp4n02-RepositoryListItemText e1wmjxnh5 MuiListItemText-dense\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body2\\"><div class=\\"css-1in239f-ClipBoardCopy eb8w2fo0\\"><span class=\\"css-7gar9h-ClipBoardCopyText eb8w2fo1\\"><a href=\\"git+https://github.com/verdaccio/ui.git\\" target=\\"_blank\\" class=\\"css-6tc7qn-GithubLink e1wmjxnh2\\">git+https://github.com/verdaccio/ui.git</a></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></span></div><span class=\\"MuiTouchRipple-root\\"></span></div></ul>"`; exports[`<Repository /> component should load the component in default state 1`] = `null`;

View File

@@ -1,40 +0,0 @@
import styled from '@emotion/styled';
import Github from '../../icons/GitHub';
import { fontWeight } from '../../utils/styles/sizes';
import Text from '../../muiComponents/Text';
import ListItem from '../../muiComponents/ListItem';
import ListItemText from '../../muiComponents/ListItemText';
import Grid from '../../muiComponents/Grid';
import { Theme } from '../../design-tokens/theme';
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
textTransform: 'capitalize',
});
export const GridRepo = styled(Grid)({
alignItems: 'center',
});
export const GithubLink = styled('a')<{ theme?: Theme }>(props => ({
color: props.theme && props.theme.palette.primary.main,
}));
export const GithubLogo = styled(Github)<{ theme?: Theme }>(props => ({
fontSize: 40,
color: props.theme && props.theme.palette.primary.main,
backgroundColor: props.theme && props.theme.palette.greySuperLight,
}));
export const RepositoryListItem = styled(ListItem)({
padding: 0,
':hover': {
backgroundColor: 'transparent',
},
});
export const RepositoryListItemText = styled(ListItemText)({
padding: '0 10px',
margin: 0,
});

View File

@@ -1,272 +1,147 @@
import React from 'react'; import React from 'react';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import '@testing-library/jest-dom/extend-expect';
import { mount, shallow } from '../../utils/test-enzyme'; import { render, fireEvent, waitForElement } from '../../utils/test-react-testing-library';
import api from '../../utils/api';
import Search from './Search'; import Search from './Search';
const SEARCH_FILE_PATH = './Search'; /* eslint-disable verdaccio/jsx-spread */
const API_FILE_PATH = '../../utils/calls'; const ComponentToBeRendered: React.FC = () => (
const URL_FILE_PATH = '../../utils/url'; <Router>
<Search />
</Router>
);
// Global mocks describe('<Search /> component', () => {
const event = {
stopPropagation: jest.fn(),
};
window.location.assign = jest.fn();
describe('<Search /> component test', () => {
let routerWrapper;
let wrapper;
beforeEach(() => { beforeEach(() => {
routerWrapper = mount( jest.resetModules();
<BrowserRouter> jest.resetAllMocks();
<Search /> jest.spyOn(api, 'request').mockImplementation(() =>
</BrowserRouter> Promise.resolve([
{
name: '@verdaccio/types',
version: '8.4.2',
},
{
name: 'verdaccio',
version: '4.3.5',
},
])
); );
}); });
test('should load the component in default state', () => { test('should load the component in default state', () => {
expect(routerWrapper.html()).toMatchSnapshot(); const { container } = render(<ComponentToBeRendered />);
expect(container.firstChild).toMatchSnapshot();
});
test('handleSearch: when user type package name in search component, show suggestions', async () => {
const { getByPlaceholderText, getAllByText } = render(<ComponentToBeRendered />);
const autoCompleteInput = getByPlaceholderText('Search Packages');
fireEvent.focus(autoCompleteInput);
fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
const suggestionsElements = await waitForElement(() => getAllByText('verdaccio', { exact: true }));
expect(suggestionsElements).toHaveLength(2);
expect(api.request).toHaveBeenCalledTimes(1);
}); });
test('onBlur: should cancel all search requests', async () => { test('onBlur: should cancel all search requests', async () => {
const Search = require(SEARCH_FILE_PATH).Search; const { getByPlaceholderText, getByRole, getAllByText } = render(<ComponentToBeRendered />);
const routerWrapper = shallow( const autoCompleteInput = getByPlaceholderText('Search Packages');
<BrowserRouter>
<Search />
</BrowserRouter>
);
wrapper = routerWrapper.find(Search).dive(); fireEvent.focus(autoCompleteInput);
const { handleOnBlur, requestList, setState } = wrapper.instance(); fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
const spyCancelAllSearchRequests = jest.spyOn(wrapper.instance(), 'cancelAllSearchRequests'); expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
setState({ search: 'verdaccio' });
const request = { const suggestionsElements = await waitForElement(() => getAllByText('verdaccio', { exact: true }));
abort: jest.fn(), expect(suggestionsElements).toHaveLength(2);
}; expect(api.request).toHaveBeenCalledTimes(1);
// adds a request for AbortController
wrapper.instance().requestList = [request];
handleOnBlur(event); fireEvent.blur(autoCompleteInput);
const listBoxElement = await waitForElement(() => getByRole('listbox'));
expect(request.abort).toHaveBeenCalled(); expect(listBoxElement).toBeEmpty();
expect(event.stopPropagation).toHaveBeenCalled();
expect(wrapper.state('error')).toBeFalsy();
expect(wrapper.state('loaded')).toBeFalsy();
expect(wrapper.state('loading')).toBeFalsy();
expect(spyCancelAllSearchRequests).toHaveBeenCalled();
expect(requestList).toEqual([]);
}); });
test('handleSearch: when user type package name in search component and set loading to true', () => { test('handleSearch: cancel all search requests when there is no value in search component with type method', async () => {
const Search = require(SEARCH_FILE_PATH).Search; const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
const routerWrapper = shallow( const autoCompleteInput = getByPlaceholderText('Search Packages');
<BrowserRouter>
<Search />
</BrowserRouter>
);
wrapper = routerWrapper.find(Search).dive(); fireEvent.focus(autoCompleteInput);
fireEvent.change(autoCompleteInput, { target: { value: ' ', method: 'type' } });
const { handleSearch } = wrapper.instance(); expect(autoCompleteInput).toHaveAttribute('value', '');
const newValue = 'verdaccio'; const listBoxElement = await waitForElement(() => getByRole('listbox'));
expect(listBoxElement).toBeEmpty();
handleSearch(event, { newValue, method: 'type' }); expect(api.request).toHaveBeenCalledTimes(0);
expect(event.stopPropagation).toHaveBeenCalled();
expect(wrapper.state('error')).toBeFalsy();
expect(wrapper.state('loaded')).toBeFalsy();
expect(wrapper.state('loading')).toBeTruthy();
expect(wrapper.state('search')).toEqual(newValue);
}); });
test('handleSearch: cancel all search requests when there is no value in search component with type method', () => { test('handleSearch: when method is not type method', async () => {
const Search = require(SEARCH_FILE_PATH).Search; const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
const routerWrapper = shallow( const autoCompleteInput = getByPlaceholderText('Search Packages');
<BrowserRouter>
<Search />
</BrowserRouter>
);
wrapper = routerWrapper.find(Search).dive(); fireEvent.focus(autoCompleteInput);
fireEvent.change(autoCompleteInput, { target: { value: ' ', method: 'click' } });
const { handleSearch, requestList } = wrapper.instance(); expect(autoCompleteInput).toHaveAttribute('value', '');
const spy = jest.spyOn(wrapper.instance(), 'cancelAllSearchRequests'); const listBoxElement = await waitForElement(() => getByRole('listbox'));
const newValue = ''; expect(listBoxElement).toBeEmpty();
expect(api.request).toHaveBeenCalledTimes(0);
handleSearch(event, { newValue, method: 'type' });
expect(event.stopPropagation).toHaveBeenCalled();
expect(wrapper.state('error')).toBeFalsy();
expect(wrapper.state('loaded')).toBeFalsy();
expect(wrapper.state('loading')).toBeTruthy();
expect(wrapper.state('search')).toEqual(newValue);
expect(spy).toHaveBeenCalled();
expect(requestList).toEqual([]);
}); });
test('handleSearch: when method is not type method', () => { test('handleSearch: loading is been displayed', async () => {
const Search = require(SEARCH_FILE_PATH).Search; const { getByPlaceholderText, getByRole, getByText } = render(<ComponentToBeRendered />);
const autoCompleteInput = getByPlaceholderText('Search Packages');
const routerWrapper = shallow( fireEvent.focus(autoCompleteInput);
<BrowserRouter> fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
<Search /> expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
</BrowserRouter>
);
wrapper = routerWrapper.find(Search).dive(); const loadingElement = await waitForElement(() => getByText('Loading...'));
expect(loadingElement).toBeTruthy();
const { handleSearch } = wrapper.instance();
const newValue = '';
handleSearch(event, { newValue, method: 'click' });
expect(event.stopPropagation).toHaveBeenCalled();
expect(wrapper.state('error')).toBeFalsy();
expect(wrapper.state('loaded')).toBeFalsy();
expect(wrapper.state('loading')).toBeFalsy();
expect(wrapper.state('search')).toEqual(newValue);
}); });
test('handlePackagesClearRequested: should clear suggestions', () => { test('handlePackagesClearRequested: should clear suggestions', async () => {
const Search = require(SEARCH_FILE_PATH).Search; const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
const autoCompleteInput = getByPlaceholderText('Search Packages');
const routerWrapper = shallow( fireEvent.focus(autoCompleteInput);
<BrowserRouter> fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
<Search /> expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
</BrowserRouter>
);
wrapper = routerWrapper.find(Search).dive(); const suggestionsElements = await waitForElement(() => getAllByText('verdaccio', { exact: true }));
expect(suggestionsElements).toHaveLength(2);
const { handlePackagesClearRequested } = wrapper.instance(); fireEvent.change(autoCompleteInput, { target: { value: ' ' } });
const listBoxElement = await waitForElement(() => getByRole('listbox'));
expect(listBoxElement).toBeEmpty();
handlePackagesClearRequested(); expect(api.request).toHaveBeenCalledTimes(2);
expect(wrapper.state('suggestions')).toEqual([]);
}); });
describe('<Search /> component: mocks specific tests ', () => { test('handleClickSearch: should change the window location on click or return key', async () => {
beforeEach(() => { const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
jest.resetModules(); const autoCompleteInput = getByPlaceholderText('Search Packages');
jest.doMock('lodash/debounce', () => {
return function debounceMock(fn) {
return fn;
};
});
});
test('handleFetchPackages: should load the packages from API', async () => { fireEvent.focus(autoCompleteInput);
const apiResponse = [{ name: 'verdaccio' }, { name: 'verdaccio-htpasswd' }]; fireEvent.change(autoCompleteInput, { target: { value: 'verdaccio' } });
const suggestions = [{ name: 'verdaccio' }, { name: 'verdaccio-htpasswd' }]; expect(autoCompleteInput).toHaveAttribute('value', 'verdaccio');
jest.doMock(API_FILE_PATH, () => ({ const suggestionsElements = await waitForElement(() => getAllByText('verdaccio', { exact: true }));
callSearch(url: string) { expect(suggestionsElements).toHaveLength(2);
return Promise.resolve(apiResponse);
},
}));
const Search = require(SEARCH_FILE_PATH).Search; // click on the second suggestion
fireEvent.click(suggestionsElements[1]);
const routerWrapper = shallow( const listBoxElement = await waitForElement(() => getByRole('listbox'));
<BrowserRouter> // when the page redirects, the list box should be empty again
<Search /> expect(listBoxElement).toBeEmpty();
</BrowserRouter>
);
wrapper = routerWrapper.find(Search).dive();
wrapper.setState({ search: 'verdaccio' });
const { handleFetchPackages } = wrapper.instance();
await handleFetchPackages({ value: 'verdaccio' });
expect(wrapper.state('suggestions')).toEqual(suggestions);
expect(wrapper.state('error')).toBeFalsy();
expect(wrapper.state('loaded')).toBeTruthy();
expect(wrapper.state('loading')).toBeFalsy();
});
test('handleFetchPackages: when browser cancel a request', async () => {
const apiResponse = { name: 'AbortError' };
jest.doMock(API_FILE_PATH, () => ({ callSearch: jest.fn(() => Promise.reject(apiResponse)) }));
const Search = require(SEARCH_FILE_PATH).Search;
const routerWrapper = shallow(
<BrowserRouter>
<Search />
</BrowserRouter>
);
wrapper = routerWrapper.find(Search).dive();
const { handleFetchPackages, setState } = wrapper.instance();
setState({ search: 'verdaccio' });
await handleFetchPackages({ value: 'verdaccio' });
expect(wrapper.state('error')).toBeFalsy();
expect(wrapper.state('loaded')).toBeFalsy();
expect(wrapper.state('loading')).toBeFalsy();
});
test('handleFetchPackages: when API server failed request', async () => {
const apiResponse = { name: 'BAD_REQUEST' };
jest.doMock(API_FILE_PATH, () => ({
callSearch(url) {
return Promise.reject(apiResponse);
},
}));
const Search = require(SEARCH_FILE_PATH).Search;
const routerWrapper = shallow(
<BrowserRouter>
<Search />
</BrowserRouter>
);
wrapper = routerWrapper.find(Search).dive();
wrapper.setState({ search: 'verdaccio' });
const { handleFetchPackages } = wrapper.instance();
await handleFetchPackages({ value: 'verdaccio' });
expect(wrapper.state('error')).toBeTruthy();
expect(wrapper.state('loaded')).toBeFalsy();
expect(wrapper.state('loading')).toBeFalsy();
});
test('handleClickSearch: should change the window location on click or return key', () => {
const getDetailPageURL = jest.fn(() => 'detail/page/url');
jest.doMock(URL_FILE_PATH, () => ({ getDetailPageURL }));
const suggestionValue = [];
const Search = require(SEARCH_FILE_PATH).Search;
const pushHandler = jest.fn();
const routerWrapper = shallow(
<BrowserRouter>
<Search history={{ push: pushHandler }} />
</BrowserRouter>
);
wrapper = routerWrapper.find(Search).dive();
const { handleClickSearch } = wrapper.instance();
// click
handleClickSearch(event, { suggestionValue, method: 'click' });
expect(event.stopPropagation).toHaveBeenCalled();
expect(pushHandler).toHaveBeenCalledTimes(1);
// return key
handleClickSearch(event, { suggestionValue, method: 'enter' });
expect(event.stopPropagation).toHaveBeenCalled();
expect(pushHandler).toHaveBeenCalledTimes(2);
});
}); });
}); });

View File

@@ -1,32 +1,12 @@
import React, { KeyboardEvent, Component, ReactElement } from 'react'; import React, { useState, FormEvent, useCallback } from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { SuggestionSelectedEventData, ChangeEvent } from 'react-autosuggest';
import styled from '@emotion/styled';
import { default as IconSearch } from '@material-ui/icons/Search';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { RouteComponentProps, withRouter } from 'react-router';
import { SuggestionSelectedEventData } from 'react-autosuggest';
import InputAdornment from '../../muiComponents/InputAdornment';
import AutoComplete from '../AutoComplete'; import AutoComplete from '../AutoComplete';
import { callSearch } from '../../utils/calls'; import { callSearch } from '../../utils/calls';
import { Theme } from '../../design-tokens/theme';
export interface State { import SearchAdornment from './SearchAdornment';
search: string;
suggestions: unknown[];
loading: boolean;
loaded: boolean;
error: boolean;
}
export type cancelAllSearchRequests = () => void;
export type handlePackagesClearRequested = () => void;
export type handleSearch = (event: React.FormEvent<HTMLInputElement>, { newValue, method }: ChangeEvent) => void;
export type handleClickSearch = (
event: KeyboardEvent<HTMLInputElement>,
{ suggestionValue, method }: { suggestionValue: object[]; method: string }
) => void;
export type handleFetchPackages = ({ value: string }) => Promise<void>;
export type onBlur = (event: React.FormEvent<HTMLInputElement>) => void;
const CONSTANTS = { const CONSTANTS = {
API_DELAY: 300, API_DELAY: 300,
@@ -34,168 +14,142 @@ const CONSTANTS = {
ABORT_ERROR: 'AbortError', ABORT_ERROR: 'AbortError',
}; };
const StyledInputAdornment = styled(InputAdornment)<{ theme?: Theme }>(props => ({ const Search: React.FC<RouteComponentProps> = ({ history }) => {
color: props.theme && props.theme.palette.white, const [suggestions, setSuggestions] = useState([]);
})); const [loaded, setLoaded] = useState(false);
const [search, setSearch] = useState('');
export class Search extends Component<RouteComponentProps<{}>, State> { const [error, setError] = useState(false);
constructor(props: RouteComponentProps<{}>) { const [loading, setLoading] = useState(false);
super(props); const [requestList, setRequestList] = useState<Array<{ abort: () => void }>>([]);
this.state = {
search: '',
suggestions: [],
// loading: A boolean value to indicate that request is in pending state.
loading: false,
// loaded: A boolean value to indicate that result has been loaded.
loaded: false,
// error: A boolean value to indicate API error.
error: false,
};
this.requestList = [];
}
public render(): ReactElement<HTMLElement> {
const { suggestions, search, loaded, loading, error } = this.state;
return (
<AutoComplete
onBlur={this.handleOnBlur}
onChange={this.handleSearch}
onCleanSuggestions={this.handlePackagesClearRequested}
onClick={this.handleClickSearch}
onSuggestionsFetch={debounce(this.handleFetchPackages, CONSTANTS.API_DELAY)}
placeholder={CONSTANTS.PLACEHOLDER_TEXT}
startAdornment={this.getAdorment()}
suggestions={suggestions}
suggestionsError={error}
suggestionsLoaded={loaded}
suggestionsLoading={loading}
value={search}
/>
);
}
/** /**
* Cancel all the requests which are in pending state. * Cancel all the requests which are in pending state.
*/ */
private cancelAllSearchRequests: cancelAllSearchRequests = () => { const cancelAllSearchRequests = useCallback(() => {
this.requestList.forEach(request => request.abort()); requestList.forEach(request => request.abort());
this.requestList = []; setRequestList([]);
}; }, [requestList, setRequestList]);
/**
* Cancel all the request from list and make request list empty.
*/
private handlePackagesClearRequested: handlePackagesClearRequested = () => {
this.setState({
suggestions: [],
});
};
/**
* onChange method for the input element.
*/
private handleSearch: handleSearch = (event, { newValue, method }) => {
// stops event bubbling
event.stopPropagation();
if (method === 'type') {
const value = newValue.trim();
this.setState(
{
search: value,
loading: true,
loaded: false,
error: false,
},
() => {
/**
* A use case where User keeps adding and removing value in input field,
* so we cancel all the existing requests when input is empty.
*/
if (value.length === 0) {
this.cancelAllSearchRequests();
}
}
);
}
};
/**
* When an user select any package by clicking or pressing return key.
*/
private handleClickSearch = (
event: React.FormEvent<HTMLInputElement>,
{ suggestionValue, method }: SuggestionSelectedEventData<unknown>
): void | undefined => {
const { history } = this.props;
// stops event bubbling
event.stopPropagation();
switch (method) {
case 'click':
case 'enter':
this.setState({ search: '' });
history.push(`/-/web/detail/${suggestionValue}`);
break;
}
};
/**
* Fetch packages from API.
* For AbortController see: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
*/
private handleFetchPackages: handleFetchPackages = async ({ value }) => {
try {
const controller = new window.AbortController();
const signal = controller.signal;
// Keep track of search requests.
this.requestList.push(controller);
const suggestions = await callSearch(value, signal);
// @ts-ignore
this.setState({
suggestions,
loaded: true,
});
} catch (error) {
/**
* AbortError is not the API error.
* It means browser has cancelled the API request.
*/
if (error.name === CONSTANTS.ABORT_ERROR) {
this.setState({ error: false, loaded: false });
} else {
this.setState({ error: true, loaded: false });
}
} finally {
this.setState({ loading: false });
}
};
private requestList: AbortController[];
public getAdorment(): JSX.Element {
return (
<StyledInputAdornment position={'start'}>
<IconSearch />
</StyledInputAdornment>
);
}
/** /**
* As user focuses out from input, we cancel all the request from requestList * As user focuses out from input, we cancel all the request from requestList
* and set the API state parameters to default boolean values. * and set the API state parameters to default boolean values.
*/ */
private handleOnBlur: onBlur = event => { const handleOnBlur = useCallback(
// stops event bubbling (event: FormEvent<HTMLInputElement>) => {
event.stopPropagation(); // stops event bubbling
this.setState( event.stopPropagation();
{ setLoaded(false);
loaded: false, setLoading(false);
loading: false, setError(false);
error: false, cancelAllSearchRequests();
}, },
() => this.cancelAllSearchRequests() [setLoaded, setLoading, cancelAllSearchRequests, setError]
); );
};
} /**
* onChange method for the input element.
*/
const handleSearch = useCallback(
(event: FormEvent<HTMLInputElement>, { newValue, method }) => {
// stops event bubbling
event.stopPropagation();
if (method === 'type') {
const value = newValue.trim();
setLoading(true);
setError(false);
setSearch(value);
setLoaded(false);
/**
* A use case where User keeps adding and removing value in input field,
* so we cancel all the existing requests when input is empty.
*/
if (value.length === 0) {
cancelAllSearchRequests();
}
}
},
[cancelAllSearchRequests]
);
/**
* Cancel all the request from list and make request list empty.
*/
const handlePackagesClearRequested = useCallback(() => {
setSuggestions([]);
}, [setSuggestions]);
/**
* When an user select any package by clicking or pressing return key.
*/
const handleClickSearch = useCallback(
(
event: FormEvent<HTMLInputElement>,
{ suggestionValue, method }: SuggestionSelectedEventData<unknown>
): void | undefined => {
// stops event bubbling
event.stopPropagation();
switch (method) {
case 'click':
case 'enter':
setSearch('');
history.push(`/-/web/detail/${suggestionValue}`);
break;
}
},
[history]
);
/**
* Fetch packages from API.
* For AbortController see: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
*/
const handleFetchPackages = useCallback(
async ({ value }: { value: string }) => {
try {
const controller = new window.AbortController();
const signal = controller.signal;
// Keep track of search requests.
setRequestList([...requestList, controller]);
const suggestions = await callSearch(value, signal);
// @ts-ignore FIXME: Argument of type 'unknown' is not assignable to parameter of type 'SetStateAction<never[]>'
setSuggestions(suggestions);
setLoaded(true);
} catch (error) {
/**
* AbortError is not the API error.
* It means browser has cancelled the API request.
*/
if (error.name === CONSTANTS.ABORT_ERROR) {
setError(false);
setLoaded(false);
} else {
setError(true);
setLoaded(false);
}
} finally {
setLoading(false);
}
},
[requestList, setRequestList, setSuggestions, setLoaded, setError, setLoading]
);
return (
<AutoComplete
onBlur={handleOnBlur}
onChange={handleSearch}
onCleanSuggestions={handlePackagesClearRequested}
onClick={handleClickSearch}
onSuggestionsFetch={debounce(handleFetchPackages, CONSTANTS.API_DELAY)}
placeholder={CONSTANTS.PLACEHOLDER_TEXT}
startAdornment={<SearchAdornment />}
suggestions={suggestions}
suggestionsError={error}
suggestionsLoaded={loaded}
suggestionsLoading={loading}
value={search}
/>
);
};
export default withRouter(Search); export default withRouter(Search);

View File

@@ -0,0 +1,18 @@
import React from 'react';
import Search from '@material-ui/icons/Search';
import styled from '@emotion/styled';
import InputAdornment from '../../muiComponents/InputAdornment';
import { Theme } from '../../design-tokens/theme';
const StyledInputAdornment = styled(InputAdornment)<{ theme?: Theme }>(props => ({
color: props.theme && props.theme.palette.white,
}));
const SearchAdornment: React.FC = () => (
<StyledInputAdornment position={'start'}>
<Search />
</StyledInputAdornment>
);
export default SearchAdornment;

View File

@@ -1,3 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Search /> component test should load the component in default state 1`] = `"<div class=\\"css-pnwf4z-Wrapper e1rflf270\\"><div role=\\"combobox\\" aria-haspopup=\\"listbox\\" aria-owns=\\"react-autowhatever-1\\" aria-expanded=\\"false\\" class=\\"react-autosuggest__container\\"><div class=\\"MuiFormControl-root MuiTextField-root react-autosuggest__input css-ae5nkp-StyledTextField e1rflf271 MuiFormControl-fullWidth\\" aria-autocomplete=\\"list\\" aria-controls=\\"react-autowhatever-1\\"><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedStart\\"><div class=\\"MuiInputAdornment-root css-1wub48n-StyledInputAdornment e1n3ivvz0 MuiInputAdornment-positionStart\\"><svg class=\\"MuiSvgIcon-root\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z\\"></path></svg></div><input aria-invalid=\\"false\\" autocomplete=\\"off\\" placeholder=\\"Search Packages\\" type=\\"text\\" class=\\"MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedStart\\" value=\\"\\"></div></div><div class=\\"MuiPaper-root MuiPaper-elevation1 react-autosuggest__suggestions-container css-kc7cda-SuggestionContainer e1rflf272\\" id=\\"react-autowhatever-1\\" role=\\"listbox\\"></div></div></div>"`; exports[`<Search /> component should load the component in default state 1`] = `
.emotion-6 {
width: 100%;
height: 32px;
position: relative;
z-index: 1;
}
.emotion-2 .MuiInputBase-root:before {
content: '';
border: none;
}
.emotion-2 .MuiInputBase-root:after {
border-color: #fff;
}
.emotion-2 .MuiInputBase-root:hover:before {
content: none;
}
.emotion-2 .MuiInputBase-input {
color: #fff;
}
.emotion-0 {
color: #fff;
}
.emotion-4 {
max-height: 500px;
overflow-y: auto;
}
<div
class="emotion-6 emotion-7"
>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-owns="react-autowhatever-1"
class="react-autosuggest__container"
role="combobox"
>
<div
aria-autocomplete="list"
aria-controls="react-autowhatever-1"
class="MuiFormControl-root MuiTextField-root react-autosuggest__input emotion-2 emotion-3 MuiFormControl-fullWidth"
>
<div
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-fullWidth MuiInput-fullWidth MuiInputBase-formControl MuiInput-formControl MuiInputBase-adornedStart"
>
<div
class="MuiInputAdornment-root emotion-0 emotion-1 MuiInputAdornment-positionStart"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
/>
</svg>
</div>
<input
aria-invalid="false"
autocomplete="off"
class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedStart"
placeholder="Search Packages"
type="text"
value=""
/>
</div>
</div>
<div
class="MuiPaper-root MuiPaper-elevation1 react-autosuggest__suggestions-container emotion-4 emotion-5"
id="react-autowhatever-1"
role="listbox"
/>
</div>
</div>
`;

View File

@@ -31,7 +31,7 @@ const UpLinks: React.FC = () => {
<ListItem key={name}> <ListItem key={name}>
<ListItemText>{name}</ListItemText> <ListItemText>{name}</ListItemText>
<Spacer /> <Spacer />
<ListItemText>{`${formatDateDistance(uplinks[name].fetched)} ago`}</ListItemText> <ListItemText>{formatDateDistance(uplinks[name].fetched)}</ListItemText>
</ListItem> </ListItem>
))} ))}
</List> </List>

View File

@@ -2,4 +2,4 @@
exports[`<UpLinks /> component should render the component when there is no uplink 1`] = `"<h6 class=\\"MuiTypography-root MuiTypography-subtitle1 MuiTypography-gutterBottom\\">verdaccio has no uplinks.</h6>"`; exports[`<UpLinks /> component should render the component when there is no uplink 1`] = `"<h6 class=\\"MuiTypography-root MuiTypography-subtitle1 MuiTypography-gutterBottom\\">verdaccio has no uplinks.</h6>"`;
exports[`<UpLinks /> component should render the component with uplinks 1`] = `"<h6 class=\\"MuiTypography-root css-5wp24z-StyledText e14i1sy10 MuiTypography-subtitle1\\">Uplinks</h6><ul class=\\"MuiList-root MuiList-padding\\"><li class=\\"MuiListItem-root MuiListItem-gutters\\"><div class=\\"MuiListItemText-root css-1pxn9ma-ListItemText e14i1sy12\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">npmjs</span></div><div class=\\"css-t1rp47-Spacer e14i1sy11\\"></div><div class=\\"MuiListItemText-root css-1pxn9ma-ListItemText e14i1sy12\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">over 1 year ago</span></div></li></ul>"`; exports[`<UpLinks /> component should render the component with uplinks 1`] = `"<h6 class=\\"MuiTypography-root css-5wp24z-StyledText e14i1sy10 MuiTypography-subtitle1\\">Uplinks</h6><ul class=\\"MuiList-root MuiList-padding\\"><li class=\\"MuiListItem-root MuiListItem-gutters\\"><div class=\\"MuiListItemText-root css-1pxn9ma-ListItemText e14i1sy12\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">npmjs</span></div><div class=\\"css-t1rp47-Spacer e14i1sy11\\"></div><div class=\\"MuiListItemText-root css-1pxn9ma-ListItemText e14i1sy12\\"><span class=\\"MuiTypography-root MuiListItemText-primary MuiTypography-body1\\">a year ago</span></div></li></ul>"`;

View File

@@ -2,11 +2,11 @@ import styled from '@emotion/styled';
import Text from '../../muiComponents/Text'; import Text from '../../muiComponents/Text';
import { default as MuiListItemText } from '../../muiComponents/ListItemText'; import { default as MuiListItemText } from '../../muiComponents/ListItemText';
import { fontWeight } from '../../utils/styles/sizes'; import { Theme } from '../../design-tokens/theme';
export const StyledText = styled(Text)({ export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: fontWeight.bold, fontWeight: props.theme && props.theme.fontWeight.bold,
}); }));
export const Spacer = styled('div')({ export const Spacer = styled('div')({
flex: '1 1 auto', flex: '1 1 auto',

View File

@@ -25,7 +25,7 @@ const VersionsHistoryList: React.FC<Props> = ({ versions, packageName, time }) =
<ListItemText>{version}</ListItemText> <ListItemText>{version}</ListItemText>
</StyledLink> </StyledLink>
<Spacer /> <Spacer />
<ListItemText>{time[version] ? `${formatDateDistance(time[version])} ago` : NOT_AVAILABLE}</ListItemText> <ListItemText>{time[version] ? formatDateDistance(time[version]) : NOT_AVAILABLE}</ListItemText>
</ListItem> </ListItem>
))} ))}
</List> </List>

View File

@@ -1,13 +1,13 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { fontWeight } from '../../utils/styles/sizes';
import Text from '../../muiComponents/Text'; import Text from '../../muiComponents/Text';
import { default as MuiListItemText } from '../../muiComponents/ListItemText'; import { default as MuiListItemText } from '../../muiComponents/ListItemText';
import { Theme } from '../../design-tokens/theme';
import Link from '../Link'; import Link from '../Link';
export const StyledText = styled(Text)({ export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: fontWeight.bold, fontWeight: props.theme && props.theme.fontWeight.bold,
}); }));
export const Spacer = styled('div')({ export const Spacer = styled('div')({
flex: '1 1 auto', flex: '1 1 auto',

View File

@@ -1,17 +1,16 @@
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/styles';
import React from 'react'; import React from 'react';
import { fontWeight } from '../utils/styles/sizes'; import { Theme } from './theme';
import { breakpoints } from '../utils/styles/media';
const resetStyles = makeStyles(() => ({ const resetStyles = makeStyles(({ theme }: { theme?: Theme }) => ({
'@global': { '@global': {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
'html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video': { 'html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video': {
fontFamily: '"Roboto", Helvetica Neue, Arial, sans-serif', fontFamily: '"Roboto", Helvetica Neue, Arial, sans-serif',
}, },
strong: { strong: {
fontWeight: fontWeight.semiBold, fontWeight: theme && theme.fontWeight.semiBold,
}, },
'html, body, #root': { 'html, body, #root': {
height: '100%', height: '100%',
@@ -25,8 +24,8 @@ const resetStyles = makeStyles(() => ({
padding: 15, padding: 15,
flex: 1, flex: 1,
[`@media screen and (min-width: ${breakpoints.container}px)`]: { [`@media screen and (min-width: ${theme && theme.breakPoints.container}px)`]: {
maxWidth: breakpoints.container, maxWidth: theme && theme.breakPoints.container,
width: '100%', width: '100%',
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',

View File

@@ -28,6 +28,44 @@ const colors = {
export type Colors = keyof typeof colors; export type Colors = keyof typeof colors;
const fontSize = {
xxl: 26,
xl: 24,
lg: 21,
md: 18,
default: 16,
sm: 14,
};
export type FontSize = keyof typeof fontSize;
const fontWeight = {
light: 300,
regular: 400,
semiBold: 500,
bold: 700,
};
export type FontWeight = keyof typeof fontWeight;
export const breakPoints = {
small: 576,
medium: 768,
large: 1024,
container: 1240,
xlarge: 1275,
};
export type BreakPoints = typeof breakPoints;
const customizedTheme = {
fontSize,
fontWeight,
breakPoints,
};
type CustomizedTheme = typeof customizedTheme;
export const theme = createMuiTheme({ export const theme = createMuiTheme({
typography: { typography: {
fontFamily: 'inherit', fontFamily: 'inherit',
@@ -38,10 +76,18 @@ export const theme = createMuiTheme({
secondary: { main: colors.secondary }, secondary: { main: colors.secondary },
error: { main: colors.red }, error: { main: colors.red },
}, },
...customizedTheme,
}); });
export type Theme = typeof theme; export type Theme = typeof theme;
declare module '@material-ui/core/styles/createMuiTheme' {
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
interface Theme extends CustomizedTheme {}
/* eslint-disable-next-line @typescript-eslint/no-empty-interface */
interface ThemeOptions extends CustomizedTheme {}
}
declare module '@material-ui/core/styles/createPalette' { declare module '@material-ui/core/styles/createPalette' {
interface CustomPalette { interface CustomPalette {
black: string; black: string;

View File

@@ -1,7 +1,6 @@
{ {
"rules": { "rules": {
"verdaccio/jsx-spread": 0, "verdaccio/jsx-spread": 0,
"react/display-name": 0,
"react/jsx-sort-props": 0 "react/jsx-sort-props": 0
} }
} }

View File

@@ -23,7 +23,6 @@ const detailContextValue = {
}; };
describe('test Version page', () => { describe('test Version page', () => {
/* eslint-disable react/jsx-max-depth */
test('should render the version page', async () => { test('should render the version page', async () => {
const { getByTestId, getByText } = render( const { getByTestId, getByText } = render(
<MemoryRouter> <MemoryRouter>

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line jest/no-mocks-import
import { import {
generateTokenWithTimeRange, generateTokenWithTimeRange,
generateTokenWithExpirationAsString, generateTokenWithExpirationAsString,
@@ -11,6 +12,7 @@ import { isTokenExpire, makeLogin } from './login';
console.error = jest.fn(); console.error = jest.fn();
jest.mock('./api', () => ({ jest.mock('./api', () => ({
// eslint-disable-next-line jest/no-mocks-import
request: require('../../jest/unit/components/__mocks__/api').default.request, request: require('../../jest/unit/components/__mocks__/api').default.request,
})); }));
@@ -40,11 +42,8 @@ describe('isTokenExpire', (): void => {
test('isTokenExpire - token is not a valid json token', (): void => { test('isTokenExpire - token is not a valid json token', (): void => {
const token = generateInvalidToken(); const token = generateInvalidToken();
const result = [ const errorToken = new SyntaxError('Unexpected token i in JSON at position 0');
'Invalid token:', const result = ['Invalid token:', errorToken, 'xxxxxx.aW52YWxpZHRva2Vu.xxxxxx'];
new SyntaxError('Unexpected token i in JSON at position 0'),
'xxxxxx.aW52YWxpZHRva2Vu.xxxxxx',
];
expect(isTokenExpire(token)).toBeTruthy(); expect(isTokenExpire(token)).toBeTruthy();
expect(console.error).toHaveBeenCalledWith(...result); expect(console.error).toHaveBeenCalledWith(...result);
}); });
@@ -60,7 +59,6 @@ describe('makeLogin', (): void => {
const result = { const result = {
error: { error: {
description: "Username or password can't be empty!", description: "Username or password can't be empty!",
title: 'Unable to login',
type: 'error', type: 'error',
}, },
}; };
@@ -78,8 +76,7 @@ describe('makeLogin', (): void => {
test('makeLogin - login should failed with 401', async () => { test('makeLogin - login should failed with 401', async () => {
const result = { const result = {
error: { error: {
description: 'bad username/password, access denied', description: 'Unable to sign in',
title: 'Unable to login',
type: 'error', type: 'error',
}, },
}; };
@@ -92,7 +89,6 @@ describe('makeLogin', (): void => {
test('makeLogin - login should failed with when no data is sent', async () => { test('makeLogin - login should failed with when no data is sent', async () => {
const result = { const result = {
error: { error: {
title: 'Unable to login',
type: 'error', type: 'error',
description: "Username or password can't be empty!", description: "Username or password can't be empty!",
}, },

View File

@@ -47,7 +47,6 @@ export interface LoginBody {
} }
export interface LoginError { export interface LoginError {
title: string;
type: string; type: string;
description: string; description: string;
} }
@@ -56,7 +55,6 @@ export async function makeLogin(username?: string, password?: string): Promise<L
// checks isEmpty // checks isEmpty
if (isEmpty(username) || isEmpty(password)) { if (isEmpty(username) || isEmpty(password)) {
const error = { const error = {
title: 'Unable to login',
type: 'error', type: 'error',
description: "Username or password can't be empty!", description: "Username or password can't be empty!",
}; };
@@ -77,10 +75,10 @@ export async function makeLogin(username?: string, password?: string): Promise<L
}; };
return result; return result;
} catch (e) { } catch (e) {
console.error('login error', e.message);
const error = { const error = {
title: 'Unable to login',
type: 'error', type: 'error',
description: e.error, description: 'Unable to sign in',
}; };
return { error }; return { error };
} }

View File

@@ -71,7 +71,7 @@ describe('formatDateDistance', (): void => {
const date2 = dateTwoMonthsAgo(); const date2 = dateTwoMonthsAgo();
// FIXME: we need to review this expect, fails every x time. // FIXME: we need to review this expect, fails every x time.
// expect(formatDateDistance(date1)).toEqual('about 2 months'); // expect(formatDateDistance(date1)).toEqual('about 2 months');
expect(formatDateDistance(date2)).toEqual('2 months'); expect(formatDateDistance(date2)).toEqual('2 months ago');
}); });
}); });

View File

@@ -2,12 +2,14 @@ import { isObject } from 'util';
import { UpLinks } from '@verdaccio/types'; import { UpLinks } from '@verdaccio/types';
import isString from 'lodash/isString'; import isString from 'lodash/isString';
import format from 'date-fns/format'; import dayjs from 'dayjs';
import formatDistanceToNow from 'date-fns/formatDistanceToNow'; import relativeTime from 'dayjs/plugin/relativeTime';
import { Time } from '../../types/packageMeta'; import { Time } from '../../types/packageMeta';
export const TIMEFORMAT = 'dd.MM.yyyy, HH:mm:ss'; export const TIMEFORMAT = 'DD.MM.YYYY, HH:mm:ss';
dayjs.extend(relativeTime);
/** /**
* Formats license field for webui. * Formats license field for webui.
@@ -52,11 +54,11 @@ export function formatRepository(repository: any): string | null {
} }
export function formatDate(lastUpdate: string | number): string { export function formatDate(lastUpdate: string | number): string {
return format(new Date(lastUpdate), TIMEFORMAT); return dayjs(new Date(lastUpdate)).format(TIMEFORMAT);
} }
export function formatDateDistance(lastUpdate: Date | string | number): string { export function formatDateDistance(lastUpdate: Date | string | number): string {
return formatDistanceToNow(new Date(lastUpdate)); return dayjs(new Date(lastUpdate)).fromNow();
} }
/** /**

View File

@@ -1,38 +0,0 @@
import { css, Interpolation } from 'emotion';
export const breakpoints = {
small: 576,
medium: 768,
large: 1024,
container: 1240,
xlarge: 1275,
};
type Breakpoints = typeof breakpoints;
type Sizes = keyof Breakpoints;
type MediaQuery<T> = {
[P in keyof T]: (args: Interpolation, key?: P) => string;
};
function constructMQ(breakpoint: Sizes, args: Interpolation): string {
const label = breakpoints[breakpoint];
const prefix = typeof label === 'string' ? '' : 'min-width:';
const suffix = typeof label === 'string' ? '' : 'px';
return css`
@media (${prefix + breakpoints[breakpoint] + suffix}) {
${args};
}
`;
}
const mq: MediaQuery<Breakpoints> = {
small: (args, b = 'small') => constructMQ(b, args),
large: (args, b = 'large') => constructMQ(b, args),
container: (args, b = 'container') => constructMQ(b, args),
medium: (args, b = 'medium') => constructMQ(b, args),
xlarge: (args, b = 'xlarge') => constructMQ(b, args),
};
export default mq;

View File

@@ -1,40 +0,0 @@
/**
* CSS to represent truncated text with an ellipsis.
*/
export function ellipsis(width: string | number): {} {
return {
display: 'inline-block',
maxWidth: width,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
wordWrap: 'normal',
};
}
/**
* Shorthand that accepts up to four values, including null to skip a value, and maps them to their respective directions.
*/
interface SpacingShortHand<type> {
top?: type;
right?: type;
bottom?: type;
left?: type;
}
const positionMap = ['Top', 'Right', 'Bottom', 'Left'];
export function spacing(property: 'padding' | 'margin', ...values: SpacingShortHand<number | string>[]): {} {
const [firstValue = 0, secondValue = 0, thirdValue = 0, fourthValue = 0] = values;
const valuesWithDefaults = [firstValue, secondValue, thirdValue, fourthValue];
let styles = {};
for (let i = 0; i < valuesWithDefaults.length; i += 1) {
if (valuesWithDefaults[i] || valuesWithDefaults[i] === 0) {
styles = {
...styles,
[`${property}${positionMap[i]}`]: valuesWithDefaults[i],
};
}
}
return styles;
}

View File

@@ -1,22 +0,0 @@
export const fontSize = {
xxl: '26px',
xl: '24px',
lg: '21px',
md: '18px',
base: '16px',
sm: '14px',
};
export const lineHeight = {
xl: '30px',
sm: '18px',
xs: '2',
xxs: '1.5',
};
export const fontWeight = {
light: 300,
regular: 400,
semiBold: 500,
bold: 700,
};

View File

@@ -1,7 +0,0 @@
// Spacings
// -------------------------
export const spacings = {
lg: '30px',
sm: '16px',
};

View File

@@ -2,8 +2,6 @@ import { mount, shallow } from 'enzyme';
import ThemeProvider from '../design-tokens/ThemeProvider'; import ThemeProvider from '../design-tokens/ThemeProvider';
/* eslint-disable @typescript-eslint/explicit-function-return-type */
const shallowWithTheme = (element: React.ReactElement<any>, ...props): any => const shallowWithTheme = (element: React.ReactElement<any>, ...props): any =>
shallow(element, { shallow(element, {
wrappingComponent: ThemeProvider, wrappingComponent: ThemeProvider,

View File

@@ -3,7 +3,6 @@ import React from 'react';
import ThemeProvider from '../design-tokens/ThemeProvider'; import ThemeProvider from '../design-tokens/ThemeProvider';
/* eslint-disable @typescript-eslint/explicit-function-return-type */
const customRender = (node: React.ReactElement<any>, ...options: Array<any>) => { const customRender = (node: React.ReactElement<any>, ...options: Array<any>) => {
return render(<ThemeProvider>{node}</ThemeProvider>, ...options); return render(<ThemeProvider>{node}</ThemeProvider>, ...options);
}; };

View File

@@ -1,7 +1,6 @@
{ {
"rules": { "rules": {
"@typescript-eslint/no-var-requires": 0, "@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-member-accessibility": 0, "@typescript-eslint/explicit-member-accessibility": 0,
"no-console": 0, "no-console": 0,

View File

@@ -30,16 +30,16 @@ describe('/ (Verdaccio Page)', () => {
await clickElement('button[data-testid="header--button-login"]'); await clickElement('button[data-testid="header--button-login"]');
await page.waitFor(500); await page.waitFor(500);
// we fill the sign in form // we fill the sign in form
const signInDialog = await page.$('#login--form-container'); const signInDialog = await page.$('#login--dialog');
const userInput = await signInDialog.$('#login--form-username'); const userInput = await signInDialog.$('#login--dialog-username');
expect(userInput).not.toBeNull(); expect(userInput).not.toBeNull();
const passInput = await signInDialog.$('#login--form-password'); const passInput = await signInDialog.$('#login--dialog-password');
expect(passInput).not.toBeNull(); expect(passInput).not.toBeNull();
await userInput.type('test', { delay: 100 }); await userInput.type('test', { delay: 100 });
await passInput.type('test', { delay: 100 }); await passInput.type('test', { delay: 100 });
await passInput.dispose(); await passInput.dispose();
// click on log in // click on log in
const loginButton = await page.$('#login--form-submit'); const loginButton = await page.$('#login--dialog-button-submit');
expect(loginButton).toBeDefined(); expect(loginButton).toBeDefined();
await loginButton.focus(); await loginButton.focus();
await loginButton.click({ delay: 100 }); await loginButton.click({ delay: 100 });
@@ -89,8 +89,7 @@ describe('/ (Verdaccio Page)', () => {
const signInButton = await page.$('button[data-testid="header--button-login"]'); const signInButton = await page.$('button[data-testid="header--button-login"]');
await signInButton.click(); await signInButton.click();
await page.waitFor(1000); await page.waitFor(1000);
const signInDialog = await page.$('#login--form-container'); const signInDialog = await page.$('#login--dialog');
expect(signInDialog).not.toBeNull(); expect(signInDialog).not.toBeNull();
}); });
// //

View File

@@ -17,7 +17,7 @@
"src", "src",
"types/*.d.ts", "types/*.d.ts",
"scripts/lib", "scripts/lib",
"node_modules/config" "node_modules/config",
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"

1
types/jest-dom.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom/extend-expect';

View File

@@ -8,6 +8,7 @@ export interface PackageMetaInterface {
dist: { dist: {
fileCount: number; fileCount: number;
unpackedSize: number; unpackedSize: number;
tarball?: string;
}; };
engines?: { engines?: {
node?: string; node?: string;
@@ -15,6 +16,14 @@ export interface PackageMetaInterface {
}; };
license?: Partial<LicenseInterface> | string; license?: Partial<LicenseInterface> | string;
version: string; version: string;
homepage?: string;
bugs?: {
url: string;
};
repository?: {
type?: string;
url?: string;
};
}; };
_uplinks: {}; _uplinks: {};
} }

1917
yarn.lock

File diff suppressed because it is too large Load Diff