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

Compare commits

...

84 Commits

Author SHA1 Message Date
Juan Picado @jotadeveloper
e4ecc4a2f9 chore(release): 0.3.10 2019-12-30 10:13:47 +01:00
Daniel Ruf
bae9638b23 fix: add missing trailing slash to publicPath - closes #395 (#396)
* fix: add missing trailing slash to publicPath - closes #395

* test: update snapshot

(cherry picked from commit fe6494fec7225928cc510e112c80e4b171160a09)

Co-authored-by: Juan Picado @jotadeveloper <juanpicado19@gmail.com>
2019-12-30 10:12:23 +01:00
Daniel Ruf
544b999f81 fix: remove whitespace from logo image - closes #374 (#400)
* fix: remove whitespace from logo image - closes #374

* test: update snapshot

Co-authored-by: Juan Picado @jotadeveloper <juanpicado19@gmail.com>
2019-12-30 10:12:00 +01:00
Juan Picado @jotadeveloper
d554049699 fix: engine warning on console for ui (#403)
* fix: engine warning on console for ui

* chore: update snapshots

* chore: remove u flag

* chore: add readme note

* chore: update README.md

Co-Authored-By: Daniel Ruf <827205+DanielRuf@users.noreply.github.com>

Co-authored-by: Daniel Ruf <827205+DanielRuf@users.noreply.github.com>
2019-12-30 10:04:42 +01:00
Daniel Ruf
787dda4a01 fix: remove background from styled Avatar components - closes #371 (#398) 2019-12-28 09:33:00 +01:00
Daniel Ruf
797c2381e4 fix: remove double padding and add missing background color - closes #373 (#399)
* fix: remove double padding - closes #373

* fix: add missing background color
2019-12-28 09:32:07 +01:00
dependabot-preview[bot]
3888a268e4 build(deps-dev): bump @types/jest from 24.0.23 to 24.0.24 (#387) 2019-12-20 15:21:47 -03:00
dependabot-preview[bot]
884d76d4a9 build(deps-dev): bump react-hook-form from 3.28.15 to 3.29.4 (#388) 2019-12-20 15:21:26 -03:00
dependabot-preview[bot]
7b55ce5ea2 build(deps-dev): bump webpack from 4.41.3 to 4.41.4 (#389) 2019-12-20 15:21:00 -03:00
dependabot-preview[bot]
1ec62de0bf build(deps-dev): bump @typescript-eslint/parser from 2.11.0 to 2.12.0 (#386) 2019-12-19 17:23:07 -03:00
dependabot-preview[bot]
3fc5c38a8e build(deps-dev): bump style-loader from 1.0.1 to 1.0.2 (#385) 2019-12-19 17:22:52 -03:00
Priscila Oliveira
53e1e63b12 chore: updated @material-ui/core (#384) 2019-12-18 19:49:56 -03:00
dependabot-preview[bot]
ef2b50a329 build(deps-dev): bump css-loader from 3.3.2 to 3.4.0 (#383) 2019-12-18 17:54:27 -03:00
dependabot-preview[bot]
61dc9b0783 build(deps-dev): bump concurrently from 5.0.1 to 5.0.2 (#379) 2019-12-18 17:33:05 -03:00
dependabot-preview[bot]
0e3391ca3d build(deps-dev): bump eslint-plugin-prettier from 3.1.1 to 3.1.2 (#381) 2019-12-18 17:32:22 -03:00
dependabot-preview[bot]
277b44ab94 build(deps-dev): bump dayjs from 1.8.17 to 1.8.18 (#380) 2019-12-18 17:32:12 -03:00
dependabot-preview[bot]
71276e15ef build(deps-dev): bump @types/node from 12.12.20 to 12.12.21 (#382) 2019-12-18 17:31:27 -03:00
Priscila Oliveira
bf093cc27b Feat: added "Fund this package" button (#375) 2019-12-18 17:30:42 -03:00
Priscila Oliveira
3a9f66c023 Refactor: Updated developers component structure (#360) 2019-12-17 22:57:53 -03:00
dependabot-preview[bot]
eef2913dd5 build(deps-dev): bump mini-css-extract-plugin from 0.8.0 to 0.8.2 (#377) 2019-12-17 21:46:04 -03:00
dependabot-preview[bot]
2af2dfe91b build(deps-dev): bump @octokit/rest from 16.35.0 to 16.35.2 (#368) 2019-12-17 19:35:36 -03:00
dependabot-preview[bot]
d4e8dff40f build(deps-dev): bump @types/node from 12.12.17 to 12.12.20 (#376) 2019-12-17 19:35:07 -03:00
dependabot-preview[bot]
cc22574100 build(deps-dev): bump @types/request from 2.48.3 to 2.48.4 (#378) 2019-12-17 19:33:39 -03:00
dependabot-preview[bot]
580b47bea1 build(deps-dev): bump @testing-library/react from 9.3.3 to 9.4.0 (#370) 2019-12-17 07:54:36 -03:00
dependabot-preview[bot]
ea24e1b50e build(deps-dev): bump webpack from 4.41.2 to 4.41.3 (#366)
Bumps [webpack](https://github.com/webpack/webpack) from 4.41.2 to 4.41.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v4.41.2...v4.41.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-16 21:43:31 +01:00
dependabot-preview[bot]
a74dc87628 build(deps-dev): bump verdaccio from 4.3.5 to 4.4.0 (#367)
Bumps [verdaccio](https://github.com/verdaccio/verdaccio) from 4.3.5 to 4.4.0.
- [Release notes](https://github.com/verdaccio/verdaccio/releases)
- [Changelog](https://github.com/verdaccio/verdaccio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/verdaccio/verdaccio/compare/v4.3.5...v4.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-16 21:24:47 +01:00
dependabot-preview[bot]
cecb54c490 build(deps-dev): bump node-mocks-http from 1.8.0 to 1.8.1 (#364)
Bumps [node-mocks-http](https://github.com/howardabrams/node-mocks-http) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/howardabrams/node-mocks-http/releases)
- [Changelog](https://github.com/howardabrams/node-mocks-http/blob/master/HISTORY.md)
- [Commits](https://github.com/howardabrams/node-mocks-http/compare/v1.8.0...v1.8.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-14 16:05:04 +01:00
dependabot-preview[bot]
b8c68314e9 build(deps-dev): bump css-loader from 3.3.0 to 3.3.2 (#365)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 3.3.0 to 3.3.2.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/v3.3.2/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v3.3.0...v3.3.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-14 16:04:45 +01:00
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
115 changed files with 10372 additions and 3337 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

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

View File

@@ -2,6 +2,52 @@
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.10](https://github.com/verdaccio/ui/compare/v0.3.9...v0.3.10) (2019-12-30)
### Features
* added "Fund this package" button ([#375](https://github.com/verdaccio/ui/issues/375)) ([bf093cc](https://github.com/verdaccio/ui/commit/bf093cc27b8625cdc50dbfc9b8dd7e37f4e24da9))
### Bug Fixes
* add missing trailing slash to publicPath - closes [#395](https://github.com/verdaccio/ui/issues/395) ([#396](https://github.com/verdaccio/ui/issues/396)) ([bae9638](https://github.com/verdaccio/ui/commit/bae9638b23b70eff78b78b8ca52ff40162333354))
* engine warning on console for ui ([#403](https://github.com/verdaccio/ui/issues/403)) ([d554049](https://github.com/verdaccio/ui/commit/d554049699494e946f4caf345177839b4f0cba8b))
* remove background from styled Avatar components - closes [#371](https://github.com/verdaccio/ui/issues/371) ([#398](https://github.com/verdaccio/ui/issues/398)) ([787dda4](https://github.com/verdaccio/ui/commit/787dda4a016a1fcd1142bd4b705e2c71e232d13e))
* remove double padding and add missing background color - closes [#373](https://github.com/verdaccio/ui/issues/373) ([#399](https://github.com/verdaccio/ui/issues/399)) ([797c238](https://github.com/verdaccio/ui/commit/797c2381e453d4f40e1703402f192eb7675d6fbe))
* remove whitespace from logo image - closes [#374](https://github.com/verdaccio/ui/issues/374) ([#400](https://github.com/verdaccio/ui/issues/400)) ([544b999](https://github.com/verdaccio/ui/commit/544b999f81e39557e0fc002d21b24c512cfebc54))
### [0.3.9](https://github.com/verdaccio/ui/compare/v0.3.8...v0.3.9) (2019-12-14)
### [0.3.8](https://github.com/verdaccio/ui/compare/v0.3.7...v0.3.8) (2019-12-14)
### Features
* login Dialog Component - Replaced class by func. comp + added react-hook-form ([#341](https://github.com/verdaccio/ui/issues/341)) ([42d3bb8](https://github.com/verdaccio/ui/commit/42d3bb8508c666c28250432ada734d58ccb0eca8))
### Bug Fixes
* formatDate ([#308](https://github.com/verdaccio/ui/issues/308)) ([33f873a](https://github.com/verdaccio/ui/commit/33f873a8c78e419a36e3a29f7ea216714172b174))
* removed deade import ([#346](https://github.com/verdaccio/ui/issues/346)) ([ae617a5](https://github.com/verdaccio/ui/commit/ae617a5c04ad1b82309d36d3bdcf6b6b6fd925d0))
* updated actionbar snap ([#340](https://github.com/verdaccio/ui/issues/340)) ([09b831a](https://github.com/verdaccio/ui/commit/09b831a40d4e82a122f8fae3e45bdd161a3281bb))
### [0.3.7](https://github.com/verdaccio/ui/compare/v0.3.6...v0.3.7) (2019-11-24)
### Features
* Added Theme and migrate to emotion@10.x 🚀 ([#286](https://github.com/verdaccio/ui/issues/286)) ([111f0c5](https://github.com/verdaccio/ui/commit/111f0c50e5053202ca55fe4f3f28dd30e4932240))
### Bug Fixes
* **#300:** correctly reference registry url from options ([ee74474](https://github.com/verdaccio/ui/commit/ee74474811eb609072e1678bcb90db33756dcf38)), closes [#300](https://github.com/verdaccio/ui/issues/300)
* restore lint-staged@8.2.1 ([dbaa0c4](https://github.com/verdaccio/ui/commit/dbaa0c43b8104b350e4907387f89d4e9e719741f))
* update snapshots ([fd306de](https://github.com/verdaccio/ui/commit/fd306def9535d9168dc79ab020ec288a4d5df1a8))
### [0.3.6](https://github.com/verdaccio/ui/compare/v0.3.5...v0.3.6) (2019-11-08) ### [0.3.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

@@ -22,7 +22,7 @@
## Contributing ## Contributing
We use `>=yarn@1.13.0`, keep on mind we use lock file. We use `>=yarn@1.13.0`, keep in mind that we use lockfiles and use at least Node `v10.13.0` to be able to build the project.
For development run the following command, it will execute `webpack` and `verdaccio` to For development run the following command, it will execute `webpack` and `verdaccio` to

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

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@verdaccio/ui-theme", "name": "@verdaccio/ui-theme",
"version": "0.3.6", "version": "0.3.10",
"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.8.0",
"@material-ui/icons": "4.5.1", "@material-ui/icons": "4.5.1",
"@octokit/rest": "16.35.0", "@octokit/rest": "16.35.2",
"@testing-library/react": "9.3.2", "@testing-library/jest-dom": "4.2.4",
"@testing-library/react": "9.4.0",
"@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.24",
"@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.21",
"@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.4",
"@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.12.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.2",
"cross-env": "6.0.3", "cross-env": "6.0.3",
"css-loader": "3.2.0", "css-loader": "3.4.0",
"date-fns": "2.8.1", "dayjs": "1.8.18",
"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.2",
"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,19 +74,20 @@
"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.2",
"node-mocks-http": "1.8.0", "mutationobserver-shim": "0.3.3",
"node-mocks-http": "1.8.1",
"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",
"ora": "4.0.3", "ora": "4.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.29.4",
"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,23 +105,23 @@
"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.2",
"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.4.0",
"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.4",
"webpack-bundle-analyzer": "3.6.0", "webpack-bundle-analyzer": "3.6.0",
"webpack-bundle-size-analyzer": "3.1.0", "webpack-bundle-size-analyzer": "3.1.0",
"webpack-cli": "3.3.10", "webpack-cli": "3.3.10",
@@ -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": ">= 8",
"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,107 @@
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 StyledBox = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
public state: AppProps = { backgroundColor: theme && theme.palette.white,
logoUrl: window.VERDACCIO_LOGO, }));
user: {},
scope: window.VERDACCIO_SCOPE || '', const StyledBoxContent = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
showLoginModal: false, [`@media screen and (min-width: ${theme && theme.breakPoints.container}px)`]: {
isUserLoggedIn: false, maxWidth: theme && theme.breakPoints.container,
packages: [], width: '100%',
isLoading: true, marginLeft: 'auto',
marginRight: 'auto',
},
}));
/* 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 />
*/ <StyledBox 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 />
</>
)}
</StyledBox>
</>
);
};
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,187 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<App /> should display the Header component 1`] = `
.emotion-10 {
background-color: #fff;
}
.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-219 emotion-10 emotion-11"
>
<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-10 {
background-color: #fff;
}
.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 emotion-10 emotion-11"
>
<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,37 +0,0 @@
import React, { FC } from 'react';
import { isEmail } from '../../utils/url';
import Tooltip from '../../muiComponents/Tooltip';
import Avatar from '../../muiComponents/Avatar';
export interface AvatarDeveloper {
name: string;
packageName: string;
version: string;
avatar: string;
email: string;
}
const AvatarTooltip: FC<AvatarDeveloper> = ({ name, packageName, version, avatar, email }) => {
const avatarComponent = <Avatar aria-label={name} src={avatar} />;
function renderLinkForMail(
email: string,
avatarComponent: JSX.Element,
packageName: string,
version: string
): JSX.Element {
if (!email || isEmail(email) === false) {
return avatarComponent;
}
return (
<a href={`mailto:${email}?subject=${packageName}@${version}`} target={'_top'}>
{avatarComponent}
</a>
);
}
return <Tooltip title={name}>{renderLinkForMail(email, avatarComponent, packageName, version)}</Tooltip>;
};
export { AvatarTooltip };

View File

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

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

View File

@@ -0,0 +1,46 @@
import React, { MouseEvent } from 'react';
import styled from '@emotion/styled';
import Favorite from '@material-ui/icons/Favorite';
import Button from '../../muiComponents/Button';
import Link from '../Link';
import { Theme } from '../../design-tokens/theme';
const StyledLink = styled(Link)<{ theme?: Theme }>(({ theme }) => ({
marginTop: theme && theme.spacing(1),
marginBottom: theme && theme.spacing(1),
textDecoration: 'none',
display: 'block',
}));
const StyledFavoriteIcon = styled(Favorite)<{ theme?: Theme }>(({ theme }) => ({
color: theme && theme.palette.orange,
}));
const StyledFundStrong = styled('strong')({
marginRight: 3,
});
interface Props {
to: string;
}
/* eslint-disable react/jsx-no-bind */
const DetailSidebarFundButton: React.FC<Props> = ({ to }) => {
const preventDefault = (event: MouseEvent<HTMLButtonElement>) => event.preventDefault();
return (
<StyledLink external={true} to={to}>
<Button
color="primary"
fullWidth={true}
onClick={preventDefault}
startIcon={<StyledFavoriteIcon />}
variant="outlined">
<StyledFundStrong>{'Fund'}</StyledFundStrong>
{'this package'}
</Button>
</StyledLink>
);
};
export default DetailSidebarFundButton;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import styled from '@emotion/styled';
import Logo from '../Logo'; import Logo from '../Logo';
@@ -8,7 +9,14 @@ interface Props {
const HeaderLogo: React.FC<Props> = ({ logo }) => { const HeaderLogo: React.FC<Props> = ({ logo }) => {
if (logo) { if (logo) {
return <img alt="logo" height="40px" src={logo} />; const Wrapper = styled('div')({
fontSize: 0,
});
return (
<Wrapper>
<img alt="logo" height="40px" src={logo} />
</Wrapper>
);
} }
return <Logo />; return <Logo />;

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

@@ -26,6 +26,9 @@ const InstallListItemText = styled(ListItemText)({
const PackageMangerAvatar = styled(Avatar)({ const PackageMangerAvatar = styled(Avatar)({
borderRadius: '0px', borderRadius: '0px',
padding: '0', padding: '0',
img: {
backgroundColor: 'transparent',
},
}); });
export enum DependencyManager { export enum DependencyManager {

View File

@@ -19,6 +19,10 @@ exports[`<Install /> renders correctly 1`] = `
padding: 0; padding: 0;
} }
.emotion-2 img {
background-color: transparent;
}
.emotion-10 { .emotion-10 {
padding: 0 10px; padding: 0 10px;
margin: 0; margin: 0;

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,84 @@
/* 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 RepositoryAvatar = styled(Avatar)({
borderRadius: '0px',
padding: '0',
img: {
backgroundColor: 'transparent',
},
});
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}>
<RepositoryAvatar 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\\">2 years 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

@@ -6,6 +6,7 @@ const colors = {
black: '#000', black: '#000',
white: '#fff', white: '#fff',
red: '#d32f2f', red: '#d32f2f',
orange: '#CD4000',
greySuperLight: '#f5f5f5', greySuperLight: '#f5f5f5',
greyLight: '#d3d3d3', greyLight: '#d3d3d3',
greyLight2: '#908ba1', greyLight2: '#908ba1',
@@ -28,6 +29,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,15 +77,24 @@ 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;
white: string; white: string;
red: string; red: string;
orange: string;
greySuperLight: string; greySuperLight: string;
greyLight: string; greyLight: string;
greyLight2: string; greyLight2: 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');
}); });
}); });

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