Compare commits

...

99 Commits

Author SHA1 Message Date
Juan Picado @jotadeveloper
e6aad5370f chore(release): 0.3.13 2020-02-02 08:37:41 +01:00
Daniel Ruf
d481f54948 fix: do not capitalize heading - closes #428 (#431) 2020-02-02 08:36:55 +01:00
dependabot-preview[bot]
e6e9cfb2b4 build(deps-dev): bump @typescript-eslint/parser from 2.15.0 to 2.18.0 (#429) 2020-02-01 22:17:53 +01:00
Juan Picado @jotadeveloper
6570e3fba1 chore: add snyk as an action (#230) 2020-01-19 09:46:48 +01:00
Alfonso Austin
d4f2720994 chore(build): add missing export (#417)
Co-authored-by: Juan Picado @jotadeveloper <juanpicado19@gmail.com>
2020-01-14 07:43:31 +01:00
Juan Picado @jotadeveloper
1eca1f4079 fix: reload packages on log in (#421)
related https://github.com/verdaccio/ui/pull/415
2020-01-14 07:16:39 +01:00
James George
164cea6c10 fix: typo (#423)
Co-authored-by: Juan Picado @jotadeveloper <juanpicado19@gmail.com>
2020-01-14 04:11:13 +01:00
James George
dad44c46c0 docs: add link to license file (#422) 2020-01-14 03:47:14 +01:00
coolsp
222ffed022 fix: package list refresh based on logged-in user (#415)
* fix: package list refresh based on logged-in user

description:
In `pages/home/Home.tsx` now monitoring any change in a user log-in/out which will trigger a new `API.request` to get the _packages_ from the Verdaccio-server.  This is done by creating a `useEffect` on **isUserLoggedIn**.  Code has been transplanted from `App/App.tsx` including the use of the Loading component during the XHR.  The use of **packages** was removed from other components as no longer needed and tests updated.

Resolves issue #414

* fix: package list refresh based on logged-in user

description:
In `pages/home/Home.tsx` now monitoring any change in a user log-in/out which will trigger a new `API.request` to get the _packages_ from the Verdaccio-server. This is done by creating a `useEffect` on **isUserLoggedIn**. Code has been transplanted from `App/App.tsx` including the use of the Loading component during the XHR. The use of **packages** was removed from other components as no longer needed and tests updated.
Test snapshots updated

Resolves issue #414

Co-authored-by: Juan Picado @jotadeveloper <juanpicado19@gmail.com>
2020-01-12 22:21:29 +01:00
Juan Picado @jotadeveloper
ee1c3f08eb fix: update dependencies (#420)
- just minor updates
- verdaccio internal deps (we know the reason of the major change)
2020-01-12 19:43:05 +01:00
Alfonso Austin
1531cb6226 chore/401 add new script to update tests (#416)
* chore: add new script to update tests #401

* chore: update script name
2020-01-11 22:22:48 +01:00
Juan Picado @jotadeveloper
e514ec95a6 chore(release): 0.3.12 2020-01-09 06:14:09 +01:00
Daniel Ruf
6b322ad553 fix: generate correct registry URL (#413)
* Revert "Revert "fix(#300): correctly reference registry url from options" (#311)"

This reverts commit d955268c25.

* fix: generate full URL from path
2020-01-09 06:12:50 +01:00
Juan Picado @jotadeveloper
3fd0154da3 chore(release): 0.3.11 2020-01-08 19:08:48 +01:00
Juan Picado @jotadeveloper
6bd38b8120 fix: remove prevent default and use react context (#411)
* fix: remove prevent default and use react context

* chore: remove string check
2020-01-08 12:16:13 +01:00
Daniel Ruf
6e2bface93 test: add SonarCloud (#408) 2020-01-03 21:51:40 +01:00
Priscila Oliveira
6eeae630ef fix: removed unused style file (#406) 2020-01-03 00:18:42 +01:00
dependabot-preview[bot]
eaea5f2501 build(deps-dev): bump style-loader from 1.0.2 to 1.1.2 (#405) 2020-01-02 21:41:19 +01:00
dependabot-preview[bot]
c1e4e739c8 build(deps-dev): bump mini-css-extract-plugin from 0.8.2 to 0.9.0 (#394)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 0.8.2 to 0.9.0.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v0.8.2...v0.9.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-02 08:38:59 +01:00
dependabot-preview[bot]
4d31aff4a4 build(deps-dev): bump @types/react from 16.9.16 to 16.9.17 (#392)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 16.9.16 to 16.9.17.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-02 08:38:37 +01:00
dependabot-preview[bot]
d9a9fc4b96 build(deps-dev): bump webpack-dev-server from 3.9.0 to 3.10.1 (#391)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 3.9.0 to 3.10.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/v3.10.1/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v3.9.0...v3.10.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-02 08:37:46 +01:00
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
124 changed files with 12291 additions and 3671 deletions

View File

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

View File

@@ -45,11 +45,8 @@
}
}
],
"@typescript-eslint/explicit-function-return-type": ["warn",
{
"allowExpressions": true,
"allowTypedFunctionExpressions": true
}],
"@typescript-eslint/explicit-function-return-type": 0,
"react/display-name": 0,
"react/no-deprecated": 1,
"react/jsx-no-target-blank": 1,
"react/destructuring-assignment": ["error", "always"],
@@ -75,7 +72,7 @@
"arrow": "parens",
"condition": "parens",
"logical": "parens",
"prop": "parens"
"prop": "ignore"
}],
"react/jsx-boolean-value": ["error", "always"],
"react/jsx-closing-tag-location": ["error"],
@@ -86,7 +83,7 @@
"react/jsx-indent": ["error", 2],
"react/jsx-indent-props": ["error", 2],
"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-no-bind": ["error"],
"react/jsx-no-comment-textnodes": ["error"],

11
.github/workflows/security.yml vendored Normal file
View File

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

0
.sonarcloud.properties Normal file
View File

View File

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

View File

@@ -2,6 +2,64 @@
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.13](https://github.com/verdaccio/ui/compare/v0.3.12...v0.3.13) (2020-02-02)
### Bug Fixes
* do not capitalize heading - closes [#428](https://github.com/verdaccio/ui/issues/428) ([#431](https://github.com/verdaccio/ui/issues/431)) ([d481f54](https://github.com/verdaccio/ui/commit/d481f549484361c1d1bc011e0858e8f99b8a2528))
* package list refresh based on logged-in user ([#415](https://github.com/verdaccio/ui/issues/415)) ([222ffed](https://github.com/verdaccio/ui/commit/222ffed0226f5aaa62f2d5b91bb08717b2aa24ef)), closes [#414](https://github.com/verdaccio/ui/issues/414) [#414](https://github.com/verdaccio/ui/issues/414)
* reload packages on log in ([#421](https://github.com/verdaccio/ui/issues/421)) ([1eca1f4](https://github.com/verdaccio/ui/commit/1eca1f40797790e87d9592204ca061527d09c4ae))
* typo ([#423](https://github.com/verdaccio/ui/issues/423)) ([164cea6](https://github.com/verdaccio/ui/commit/164cea6c10804c1d2097c2a582eb3e1e51814d4a))
* update dependencies ([#420](https://github.com/verdaccio/ui/issues/420)) ([ee1c3f0](https://github.com/verdaccio/ui/commit/ee1c3f08eb16da2313d8841cfab18358d7f4ea10))
### [0.3.12](https://github.com/verdaccio/ui/compare/v0.3.11...v0.3.12) (2020-01-09)
### Bug Fixes
* generate correct registry URL ([#413](https://github.com/verdaccio/ui/issues/413)) ([6b322ad](https://github.com/verdaccio/ui/commit/6b322ad553e9fb3ee65b2968dcfe856ba42a0bfb)), closes [#300](https://github.com/verdaccio/ui/issues/300) [#311](https://github.com/verdaccio/ui/issues/311)
### [0.3.11](https://github.com/verdaccio/ui/compare/v0.3.10...v0.3.11) (2020-01-08)
### Bug Fixes
* remove prevent default and use react context ([#411](https://github.com/verdaccio/ui/issues/411)) ([6bd38b8](https://github.com/verdaccio/ui/commit/6bd38b812032857bb19af8978d48f6f8969af6cf))
* removed unused style file ([#406](https://github.com/verdaccio/ui/issues/406)) ([6eeae63](https://github.com/verdaccio/ui/commit/6eeae630ef441a871d06b888b6a21178e36e0db7))
### [0.3.10](https://github.com/verdaccio/ui/compare/v0.3.9...v0.3.10) (2019-12-30)
### Features
* added "Fund this package" button ([#375](https://github.com/verdaccio/ui/issues/375)) ([bf093cc](https://github.com/verdaccio/ui/commit/bf093cc27b8625cdc50dbfc9b8dd7e37f4e24da9))
### Bug Fixes
* add missing trailing slash to publicPath - closes [#395](https://github.com/verdaccio/ui/issues/395) ([#396](https://github.com/verdaccio/ui/issues/396)) ([bae9638](https://github.com/verdaccio/ui/commit/bae9638b23b70eff78b78b8ca52ff40162333354))
* engine warning on console for ui ([#403](https://github.com/verdaccio/ui/issues/403)) ([d554049](https://github.com/verdaccio/ui/commit/d554049699494e946f4caf345177839b4f0cba8b))
* remove background from styled Avatar components - closes [#371](https://github.com/verdaccio/ui/issues/371) ([#398](https://github.com/verdaccio/ui/issues/398)) ([787dda4](https://github.com/verdaccio/ui/commit/787dda4a016a1fcd1142bd4b705e2c71e232d13e))
* remove double padding and add missing background color - closes [#373](https://github.com/verdaccio/ui/issues/373) ([#399](https://github.com/verdaccio/ui/issues/399)) ([797c238](https://github.com/verdaccio/ui/commit/797c2381e453d4f40e1703402f192eb7675d6fbe))
* remove whitespace from logo image - closes [#374](https://github.com/verdaccio/ui/issues/374) ([#400](https://github.com/verdaccio/ui/issues/400)) ([544b999](https://github.com/verdaccio/ui/commit/544b999f81e39557e0fc002d21b24c512cfebc54))
### [0.3.9](https://github.com/verdaccio/ui/compare/v0.3.8...v0.3.9) (2019-12-14)
### [0.3.8](https://github.com/verdaccio/ui/compare/v0.3.7...v0.3.8) (2019-12-14)
### Features
* login Dialog Component - Replaced class by func. comp + added react-hook-form ([#341](https://github.com/verdaccio/ui/issues/341)) ([42d3bb8](https://github.com/verdaccio/ui/commit/42d3bb8508c666c28250432ada734d58ccb0eca8))
### Bug Fixes
* formatDate ([#308](https://github.com/verdaccio/ui/issues/308)) ([33f873a](https://github.com/verdaccio/ui/commit/33f873a8c78e419a36e3a29f7ea216714172b174))
* removed deade import ([#346](https://github.com/verdaccio/ui/issues/346)) ([ae617a5](https://github.com/verdaccio/ui/commit/ae617a5c04ad1b82309d36d3bdcf6b6b6fd925d0))
* updated actionbar snap ([#340](https://github.com/verdaccio/ui/issues/340)) ([09b831a](https://github.com/verdaccio/ui/commit/09b831a40d4e82a122f8fae3e45bdd161a3281bb))
### [0.3.7](https://github.com/verdaccio/ui/compare/v0.3.6...v0.3.7) (2019-11-24)

View File

@@ -12,7 +12,7 @@
[![stackshare](https://img.shields.io/badge/Follow%20on-StackShare-blue.svg?logo=stackshare&style=flat)](https://stackshare.io/verdaccio)
[![discord](https://img.shields.io/discord/388674437219745793.svg)](http://chat.verdaccio.org/)
[![node](https://img.shields.io/node/v/@verdaccio/ui-theme/latest.svg)](https://www.npmjs.com/package/@verdaccio/ui-theme)
![MIT](https://img.shields.io/github/license/mashape/apistatus.svg)
[![MIT](https://img.shields.io/github/license/mashape/apistatus.svg)](./LICENSE)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/verdaccio/localized.svg)](https://crowdin.com/project/verdaccio)
[![codecov](https://codecov.io/gh/verdaccio/ui/branch/master/graph/badge.svg)](https://codecov.io/gh/verdaccio/ui)
@@ -22,7 +22,7 @@
## Contributing
We use `>=yarn@1.13.0`, keep 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

View File

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

View File

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

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 addHours from 'date-fns/addHours';
import dayjs from 'dayjs';
export function generateTokenWithTimeRange(limit = 0) {
export function generateTokenWithTimeRange(amount = 0) {
const payload = {
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`;
}

View File

@@ -192,7 +192,7 @@ export const packageMeta = {
jest: { snapshotSerializers: ['jest-serializer-enzyme'] },
engines: { node: '>=4.6.1', npm: '>=2.15.9' },
preferGlobal: true,
publishConfig: { registry: 'http://localhost:4873/' },
publishConfig: { registry: 'https://registry.verdaccio.org' },
license: 'WTFPL',
contributors: [
{
@@ -578,7 +578,7 @@ export const packageMeta = {
_npmUser: {},
dist: {
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",
"version": "0.3.7",
"version": "0.3.13",
"description": "Verdaccio User Interface",
"author": {
"name": "Verdaccio Core Team",
@@ -13,58 +13,59 @@
"homepage": "https://verdaccio.org",
"main": "index.js",
"devDependencies": {
"@babel/plugin-proposal-nullish-coalescing-operator": "7.7.4",
"@babel/plugin-proposal-optional-chaining": "7.7.4",
"@commitlint/cli": "8.2.0",
"@commitlint/config-conventional": "8.2.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.0",
"@babel/plugin-proposal-optional-chaining": "7.8.0",
"@commitlint/cli": "8.3.4",
"@commitlint/config-conventional": "8.3.4",
"@emotion/core": "10.0.22",
"@emotion/styled": "10.0.23",
"@material-ui/core": "4.6.1",
"@material-ui/core": "4.8.0",
"@material-ui/icons": "4.5.1",
"@octokit/rest": "16.35.0",
"@testing-library/react": "9.3.2",
"@octokit/rest": "16.35.2",
"@testing-library/jest-dom": "4.2.4",
"@testing-library/react": "9.4.0",
"@types/autosuggest-highlight": "3.1.0",
"@types/enzyme": "3.10.3",
"@types/jest": "24.0.23",
"@types/enzyme": "3.10.4",
"@types/jest": "24.0.24",
"@types/js-base64": "2.3.1",
"@types/lodash": "4.14.149",
"@types/node": "12.12.11",
"@types/react": "16.9.11",
"@types/node": "13.1.6",
"@types/react": "16.9.17",
"@types/react-autosuggest": "9.3.13",
"@types/react-dom": "16.9.4",
"@types/react-router-dom": "5.1.2",
"@types/request": "2.48.3",
"@types/validator": "12.0.0",
"@types/webpack-env": "1.14.1",
"@typescript-eslint/parser": "2.8.0",
"@verdaccio/babel-preset": "8.4.2",
"@verdaccio/commons-api": "8.4.2",
"@types/react-router-dom": "5.1.3",
"@types/request": "2.48.4",
"@types/validator": "12.0.1",
"@types/webpack-env": "1.15.0",
"@typescript-eslint/parser": "2.18.0",
"@verdaccio/babel-preset": "9.0.0",
"@verdaccio/commons-api": "9.0.0",
"@verdaccio/eslint-config": "8.4.2",
"@verdaccio/types": "8.4.2",
"@verdaccio/types": "9.0.0",
"autosuggest-highlight": "3.1.1",
"babel-loader": "8.0.6",
"bundlesize": "0.18.0",
"codeceptjs": "2.3.5",
"codeceptjs": "2.4.0",
"codecov": "3.6.1",
"concurrently": "5.0.0",
"concurrently": "5.0.2",
"cross-env": "6.0.3",
"css-loader": "3.2.0",
"date-fns": "2.8.1",
"css-loader": "3.4.2",
"dayjs": "1.8.19",
"detect-secrets": "1.0.5",
"emotion": "10.0.23",
"emotion-theming": "10.0.19",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.1",
"emotion": "10.0.27",
"emotion-theming": "10.0.27",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.2",
"enzyme-to-json": "3.4.3",
"eslint": "6.7.0",
"eslint": "6.7.2",
"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-prettier": "3.1.1",
"eslint-plugin-react": "7.16.0",
"eslint-plugin-prettier": "3.1.2",
"eslint-plugin-react": "7.17.0",
"eslint-plugin-react-hooks": "2.3.0",
"eslint-plugin-verdaccio": "8.4.2",
"file-loader": "4.3.0",
"file-loader": "5.0.2",
"friendly-errors-webpack-plugin": "1.7.0",
"get-stdin": "7.0.0",
"github-markdown-css": "3.0.1",
@@ -73,19 +74,20 @@
"identity-obj-proxy": "3.0.0",
"in-publish": "2.0.0",
"jest": "24.9.0",
"jest-emotion": "10.0.17",
"jest-emotion": "10.0.27",
"jest-environment-jsdom": "24.9.0",
"jest-environment-jsdom-global": "1.2.0",
"jest-environment-node": "24.9.0",
"jest-fetch-mock": "2.1.2",
"jest-fetch-mock": "3.0.1",
"js-base64": "2.5.1",
"js-yaml": "3.13.1",
"lint-staged": "9.4.3",
"lint-staged": "9.5.0",
"localstorage-memory": "1.0.3",
"lockfile-lint": "2.2.0",
"lockfile-lint": "3.0.5",
"lodash": "^4.17.15",
"mini-css-extract-plugin": "0.8.0",
"node-mocks-http": "1.8.0",
"mini-css-extract-plugin": "0.9.0",
"mutationobserver-shim": "0.3.3",
"node-mocks-http": "1.8.1",
"normalize.css": "8.0.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
"ora": "4.0.3",
@@ -95,6 +97,7 @@
"react": "16.12.0",
"react-autosuggest": "9.4.3",
"react-dom": "16.12.0",
"react-hook-form": "3.29.4",
"react-hot-loader": "4.12.18",
"react-router-dom": "5.1.2",
"request": "2.88.0",
@@ -102,27 +105,27 @@
"rimraf": "3.0.0",
"source-map-loader": "0.2.4",
"standard-version": "7.0.1",
"style-loader": "1.0.0",
"style-loader": "1.1.2",
"stylelint": "12.0.0",
"stylelint-config-recommended": "3.0.0",
"stylelint-config-styled-components": "0.1.1",
"stylelint-processor-styled-components": "1.8.0",
"stylelint-webpack-plugin": "1.1.0",
"stylelint-processor-styled-components": "1.9.0",
"stylelint-webpack-plugin": "1.1.2",
"supertest": "4.0.2",
"typeface-roboto": "0.0.75",
"typescript": "3.7.2",
"typescript": "3.7.4",
"uglifyjs-webpack-plugin": "2.2.0",
"url-loader": "2.3.0",
"url-loader": "3.0.0",
"validator": "12.1.0",
"verdaccio": "4.3.5",
"verdaccio-auth-memory": "8.4.2",
"verdaccio-memory": "8.4.2",
"verdaccio": "4.4.2",
"verdaccio-auth-memory": "9.0.0",
"verdaccio-memory": "9.0.0",
"wait-on": "3.3.0",
"webpack": "4.41.2",
"webpack": "4.41.5",
"webpack-bundle-analyzer": "3.6.0",
"webpack-bundle-size-analyzer": "3.1.0",
"webpack-cli": "3.3.10",
"webpack-dev-server": "3.9.0",
"webpack-dev-server": "3.10.1",
"webpack-merge": "4.2.2",
"whatwg-fetch": "3.0.0",
"xss": "1.0.6"
@@ -135,7 +138,7 @@
"bundlesize": [
{
"path": "./static/vendors.*.js",
"maxSize": "180 kB"
"maxSize": "185 kB"
},
{
"path": "./static/main.*.js",
@@ -168,6 +171,7 @@
"test:acceptance:server": "concurrently --kill-others \"npm run verdaccio:server\" \"npm run test:acceptance\"",
"test:e2e": "cross-env BABEL_ENV=test jest --config ./test/jest.config.e2e.js",
"test": "cross-env NODE_ENV=test BABEL_ENV=test TZ=UTC jest --config ./jest/jest.config.js --maxWorkers 2 --passWithNoTests",
"test:update-snapshot": "npm run test -- -u",
"test:size": "bundlesize",
"lint": "npm run lint:js && npm run lint:css && npm run lint:lockfile",
"lint:js": "npm run type-check && eslint . --ext .js,.ts,.tsx",
@@ -183,7 +187,7 @@
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run verdaccio:server\""
},
"engines": {
"node": ">=8",
"node": ">= 8",
"npm": ">=5"
},
"husky": {
@@ -212,6 +216,5 @@
"type": "opencollective",
"url": "https://opencollective.com/verdaccio",
"logo": "https://opencollective.com/verdaccio/logo.txt"
},
"dependencies": {}
}
}

View File

@@ -5,38 +5,35 @@ import { Router } from 'react-router-dom';
import storage from '../utils/storage';
import { isTokenExpire } from '../utils/login';
import API from '../utils/api';
import Header from '../components/Header';
import Footer from '../components/Footer';
import Box from '../muiComponents/Box';
import Loading from '../components/Loading';
import StyleBaseline from '../design-tokens/StyleBaseline';
import { breakpoints } from '../utils/styles/media';
import { Theme } from '../design-tokens/theme';
import AppContextProvider from './AppContextProvider';
import AppRoute, { history } from './AppRoute';
const StyledBoxContent = styled(Box)({
padding: 15,
[`@media screen and (min-width: ${breakpoints.container}px)`]: {
maxWidth: breakpoints.container,
const StyledBox = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor: theme && theme.palette.white,
}));
const StyledBoxContent = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
[`@media screen and (min-width: ${theme && theme.breakPoints.container}px)`]: {
maxWidth: theme && theme.breakPoints.container,
width: '100%',
marginLeft: 'auto',
marginRight: 'auto',
},
});
}));
/* eslint-disable react/jsx-max-depth */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* 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
* Logout user
* Required by: <Header />
*/
const logout = () => {
@@ -58,47 +55,27 @@ const App: React.FC = () => {
setUser({ username });
};
const loadOnHandler = async () => {
try {
const packages = await API.request('packages', 'GET');
// FIXME add correct type for package
setPackages(packages as never[]);
} catch (error) {
// FIXME: add dialog
console.error({
title: 'Warning',
message: `Unable to load package list: ${error.message}`,
});
}
setIsLoading(false);
};
useEffect(() => {
checkUserAlreadyLoggedIn();
loadOnHandler();
}, []);
return (
<>
<StyleBaseline />
<Box display="flex" flexDirection="column" height="100%">
{isLoading ? (
<Loading />
) : (
<>
<Router history={history}>
<AppContextProvider packages={packages} user={user}>
<Header />
<StyledBoxContent flexGrow={1}>
<AppRoute />
</StyledBoxContent>
</AppContextProvider>
</Router>
<Footer />
</>
)}
</Box>
<StyledBox display="flex" flexDirection="column" height="100%">
<>
<Router history={history}>
<AppContextProvider user={user}>
<Header />
<StyledBoxContent flexGrow={1}>
{/* eslint-disable-next-line react/jsx-max-depth */}
<AppRoute />
</StyledBoxContent>
</AppContextProvider>
</Router>
<Footer />
</>
</StyledBox>
</>
);
};

View File

@@ -1,12 +1,8 @@
import { createContext } from 'react';
import { FormError } from '../components/Login/Login';
export interface AppProps {
error?: FormError;
user?: User;
scope: string;
packages: any[];
}
export interface User {
@@ -15,7 +11,6 @@ export interface User {
export interface AppContextProps extends AppProps {
setUser: (user?: User) => void;
setError: (error?: FormError) => void;
}
const AppContext = createContext<undefined | AppContextProps>(undefined);

View File

@@ -1,20 +1,15 @@
import React, { useState, useEffect } from 'react';
import { FormError } from '../components/Login/Login';
import AppContext, { AppProps, User } from './AppContext';
interface Props {
packages: any[];
user?: User;
}
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable react-hooks/exhaustive-deps */
const AppContextProvider: React.FC<Props> = ({ children, packages, user }) => {
const AppContextProvider: React.FC<Props> = ({ children, user }) => {
const [state, setState] = useState<AppProps>({
scope: window.VERDACCIO_SCOPE || '',
packages,
user,
});
@@ -25,13 +20,6 @@ const AppContextProvider: React.FC<Props> = ({ children, packages, user }) => {
});
}, [user]);
useEffect(() => {
setState({
...state,
packages,
});
}, [packages]);
const setUser = (user?: User) => {
setState({
...state,
@@ -39,19 +27,11 @@ const AppContextProvider: React.FC<Props> = ({ children, packages, user }) => {
});
};
const setError = (error?: FormError) => {
setState({
...state,
error,
});
};
return (
<AppContext.Provider
value={{
...state,
setUser,
setError,
}}>
{children}
</AppContext.Provider>

View File

@@ -23,7 +23,6 @@ export const history = createBrowserHistory({
basename: window.__VERDACCIO_BASENAME_UI_OPTIONS && window.__VERDACCIO_BASENAME_UI_OPTIONS.url_prefix,
});
/* eslint react/jsx-max-depth: 0 */
const AppRoute: React.FC = () => {
const appContext = useContext(AppContext);
@@ -31,7 +30,7 @@ const AppRoute: React.FC = () => {
throw Error('The app Context was not correct used');
}
const { user, packages } = appContext;
const { user } = appContext;
const isUserLoggedIn = user && user.username;
@@ -40,7 +39,7 @@ const AppRoute: React.FC = () => {
<Suspense fallback={<Loading />}>
<Switch>
<ReactRouterDomRoute exact={true} path={Route.ROOT}>
<HomePage isUserLoggedIn={!!isUserLoggedIn} packages={packages || []} />
<HomePage isUserLoggedIn={!!isUserLoggedIn} />
</ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE}>
<VersionContextProvider>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,133 +1,44 @@
import React, { Component, ReactElement } from 'react';
import BugReportIcon from '@material-ui/icons/BugReport';
import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import React from 'react';
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/Version';
import { isURL, extractFileName, downloadFile } from '../../utils/url';
import api from '../../utils/api';
import Tooltip from '../../muiComponents/Tooltip';
import List from '../../muiComponents/List';
import { DetailContext } from '../../pages/Version';
import { isURL } from '../../utils/url';
import Box from '../../muiComponents/Box';
import { Fab, ActionListItem } from './styles';
import ActionBarAction, { ActionBarActionProps } from './ActionBarAction';
export interface Action {
icon: string;
title: string;
handler?: Function;
}
/* eslint-disable verdaccio/jsx-spread */
const ActionBar: React.FC = () => {
const detailContext = React.useContext(DetailContext);
export async function downloadHandler(link: string): Promise<void> {
const fileStream: Blob = await api.request(link, 'GET', {
headers: {
['accept']:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
},
credentials: 'include',
});
const fileName = extractFileName(link);
downloadFile(fileStream, fileName);
}
const { packageMeta } = detailContext;
const ACTIONS = {
homepage: {
icon: <HomeIcon />,
title: 'Visit homepage',
},
issue: {
icon: <BugReportIcon />,
title: 'Open an issue',
},
tarball: {
icon: <DownloadIcon />,
title: 'Download tarball',
handler: downloadHandler,
},
if (!packageMeta?.latest) {
return null;
}
const { homepage, bugs, dist } = packageMeta.latest;
const actions: Array<ActionBarActionProps> = [];
if (homepage && isURL(homepage)) {
actions.push({ type: 'VISIT_HOMEPAGE', link: homepage });
}
if (bugs?.url && isURL(bugs.url)) {
actions.push({ type: 'OPEN_AN_ISSUE', link: bugs.url });
}
if (dist?.tarball && isURL(dist.tarball)) {
actions.push({ type: 'DOWNLOAD_TARBALL', link: dist.tarball });
}
return (
<Box alignItems="center" display="flex" marginBottom="8px">
{actions.map(action => (
<ActionBarAction key={action.link} {...action} />
))}
</Box>
);
};
class ActionBar extends Component {
public render(): ReactElement<HTMLElement> {
return (
<DetailContextConsumer>
{context => {
const { packageMeta } = context;
if (!packageMeta) {
return null;
}
return this.renderActionBar(context as VersionPageConsumerProps);
}}
</DetailContextConsumer>
);
}
private renderIconsWithLink(link: string, component: JSX.Element): ReactElement<HTMLElement> {
return (
<a href={link} target={'_blank'}>
{component}
</a>
);
}
private renderActionBar = ({ packageMeta }) => {
const { latest } = packageMeta;
if (!latest) {
return null;
}
const { homepage, bugs, dist } = latest;
const actionsMap = {
homepage,
issue: bugs ? bugs.url : null,
tarball: dist ? dist.tarball : null,
};
const renderList = Object.keys(actionsMap).reduce((component: React.ReactElement[], value, key) => {
const link = actionsMap[value];
if (link && isURL(link)) {
const actionItem: Action = ACTIONS[value];
if (actionItem.handler) {
const fab = (
<Tooltip key={key} title={actionItem['title']}>
<Fab
/* eslint-disable react/jsx-no-bind */
onClick={() => {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
actionItem.handler!(link);
}}
size={'small'}>
{actionItem['icon']}
</Fab>
</Tooltip>
);
component.push(fab);
} else {
const fab = <Fab size={'small'}>{actionItem['icon']}</Fab>;
component.push(
<Tooltip key={key} title={actionItem['title']}>
<>{this.renderIconsWithLink(link, fab)}</>
</Tooltip>
);
}
}
return component;
}, []);
if (renderList.length > 0) {
return (
<List>
<ActionListItem alignItems={'flex-start'} button={true}>
{renderList}
</ActionListItem>
</List>
);
}
return null;
};
}
export { ActionBar };
export default 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
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 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 { fontWeight } from '../../utils/styles/sizes';
import ListItem from '../../muiComponents/ListItem';
import Text from '../../muiComponents/Text';
import ListItemText from '../../muiComponents/ListItemText';
import { Theme } from '../../design-tokens/theme';
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
textTransform: 'capitalize',
});
}));
export const AuthorListItem = styled(ListItem)({
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 Autosuggest, { SuggestionSelectedEventData, InputProps, ChangeEvent } from 'react-autosuggest';
import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse';
import { fontWeight } from '../../utils/styles/sizes';
import MenuItem from '../../muiComponents/MenuItem';
import { Theme } from '../../design-tokens/theme';
import { Wrapper, InputField, SuggestionContainer } from './styles';
const StyledAnchor = styled('a')<{ fw: number }>(props => ({
fontWeight: props.fw,
const StyledAnchor = styled('a')<{ highlight: boolean; theme?: Theme }>(props => ({
fontWeight: props.theme && props.highlight ? props.theme.fontWeight.semiBold : props.theme.fontWeight.light,
}));
const StyledMenuItem = styled(MenuItem)({
@@ -64,9 +64,8 @@ const renderSuggestion = (suggestion, { query, isHighlighted }): JSX.Element =>
<StyledMenuItem component="div" selected={isHighlighted}>
<div>
{parts.map((part, index) => {
const fw = part.highlight ? fontWeight.semiBold : fontWeight.light;
return (
<StyledAnchor fw={fw} key={String(index)}>
<StyledAnchor highlight={part.highlight} key={String(index)}>
{part.text}
</StyledAnchor>
);
@@ -90,64 +89,66 @@ const SUGGESTIONS_RESPONSE = {
NO_RESULT: 'No results found.',
};
const AutoComplete = ({
suggestions,
startAdornment,
onChange,
onSuggestionsFetch,
onCleanSuggestions,
value = '',
placeholder = '',
disableUnderline = false,
onClick,
onKeyDown,
onBlur,
suggestionsLoading = false,
suggestionsLoaded = false,
suggestionsError = false,
}: Props): JSX.Element => {
const autosuggestProps = {
renderInputComponent,
const AutoComplete = memo(
({
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,
onChange,
onSuggestionsFetch,
onCleanSuggestions,
value = '',
placeholder = '',
disableUnderline = false,
onClick,
onKeyDown,
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 (
<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>
<Wrapper>
<Autosuggest
{...autosuggestProps}
inputProps={inputProps}
onSuggestionSelected={onClick}
renderSuggestionsContainer={renderSuggestionsContainer}
/>
</Wrapper>
);
}
return (
<Wrapper>
<Autosuggest
{...autosuggestProps}
inputProps={inputProps}
onSuggestionSelected={onClick}
renderSuggestionsContainer={renderSuggestionsContainer}
/>
</Wrapper>
);
};
);
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 { fontWeight } from '../../utils/styles/sizes';
import Text from '../../muiComponents/Text';
import Card from '../../muiComponents/Card';
import Chip from '../../muiComponents/Chip';
import { Theme } from '../../design-tokens/theme';
export const CardWrap = styled(Card)({
margin: '0 0 16px',
});
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
textTransform: 'capitalize',
});
}));
export const Tags = styled('div')({
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 List from '../../muiComponents/List';
import Card from '../../muiComponents/Card';
import CardContent from '../../muiComponents/CardContent';
import Paper from '../../muiComponents/Paper';
import ActionBar from '../ActionBar';
import Repository from '../Repository';
import Engines from '../Engines';
import Dist from '../Dist';
import Install from '../Install';
import 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 => {
return (
<>
<PackageDescription>{description}</PackageDescription>
{version ? (
<PackageVersion>
<small>{`${isLatest ? 'Latest v' : 'v'}${version}`}</small>
</PackageVersion>
) : null}
</>
);
};
const StyledPaper = styled(Paper)<{ theme?: Theme }>(({ theme }) => ({
padding: theme.spacing(3, 2),
}));
const renderCopyCLI = (): JSX.Element => <Install />;
const renderMaintainers = (): JSX.Element => <Developers type="maintainers" />;
const renderContributors = (): JSX.Element => <Developers type="contributors" />;
const renderRepository = (): JSX.Element => <Repository />;
const renderAuthor = (): JSX.Element => <Author />;
const renderEngine = (): JSX.Element => <Engine />;
const renderDist = (): JSX.Element => <Dist />;
const renderActionBar = (): JSX.Element => <ActionBar />;
const renderTitle = (packageName, packageVersion, packageMeta): JSX.Element => {
const version = packageVersion ? packageVersion : packageMeta.latest.version;
const isLatest = typeof packageVersion === 'undefined';
const DetailSidebar: React.FC = () => {
const detailContext = useContext(DetailContext);
const { packageMeta, packageName, packageVersion } = detailContext;
if (!packageMeta || !packageName) {
return null;
}
return (
<List className="detail-info">
<TitleListItem alignItems="flex-start" button={true}>
<TitleListItemText
primary={<b>{packageName}</b>}
secondary={renderLatestDescription(packageMeta.latest.description, version, isLatest)}
/>
</TitleListItem>
</List>
<StyledPaper className={'sidebar-info'}>
<DetailSidebarTitle
description={packageMeta.latest?.description}
isLatest={typeof packageVersion === 'undefined'}
packageName={packageName}
version={packageVersion || packageMeta.latest.version}
/>
<ActionBar />
<Install />
<DetailSidebarFundButton />
<Repository />
<Engines />
<Dist />
<Author />
<Developers type={DeveloperType.MAINTAINERS} />
<Developers type={DeveloperType.CONTRIBUTORS} />
</StyledPaper>
);
};
function renderSideBar(packageName, packageVersion, packageMeta): ReactElement<HTMLElement> {
return (
<div className={'sidebar-info'}>
<Card>
<CardContent>
{renderTitle(packageName, packageVersion, packageMeta)}
{renderActionBar()}
{renderCopyCLI()}
{renderRepository()}
{renderEngine()}
{renderDist()}
{renderAuthor()}
{renderMaintainers()}
{renderContributors()}
</CardContent>
</Card>
</div>
);
}
const DetailSidebar = (): JSX.Element => {
const { packageName, packageMeta, packageVersion } = React.useContext(DetailContext);
return renderSideBar(packageName, packageVersion, packageMeta);
};
export default DetailSidebar;

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
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,
});
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 { DetailContextProvider } from '../../pages/Version';
import Developers, { DevelopersType } from './Developers';
import { Fab } from './styles';
import Developers, { DeveloperType, Fab } from './Developers';
describe('test Developers', () => {
const packageMeta = {
@@ -35,14 +34,13 @@ describe('test Developers', () => {
};
test('should render the component with no items', () => {
const type: DevelopersType = 'maintainers';
const packageMeta = {
latest: {},
};
const wrapper = mount(
// @ts-ignore
<DetailContextProvider value={{ packageMeta }}>
<Developers type={type} />
<Developers type={DeveloperType.MAINTAINERS} />
</DetailContextProvider>
);
@@ -50,11 +48,10 @@ describe('test Developers', () => {
});
test('should render the component for maintainers with items', () => {
const type: DevelopersType = 'maintainers';
const wrapper = mount(
// @ts-ignore
<DetailContextProvider value={{ packageMeta }}>
<Developers type={type} />
<Developers type={DeveloperType.MAINTAINERS} />
</DetailContextProvider>
);
@@ -62,11 +59,10 @@ describe('test Developers', () => {
});
test('should render the component for contributors with items', () => {
const type: DevelopersType = 'contributors';
const wrapper = mount(
// @ts-ignore
<DetailContextProvider value={{ packageMeta }}>
<Developers type={type} />
<Developers type={DeveloperType.CONTRIBUTORS} />
</DetailContextProvider>
);
@@ -74,7 +70,6 @@ describe('test Developers', () => {
});
test('should test onClick the component avatar', () => {
const type: DevelopersType = 'contributors';
const packageMeta = {
latest: {
packageName: 'foo',
@@ -95,7 +90,7 @@ describe('test Developers', () => {
const wrapper = mount(
// @ts-ignore
<DetailContextProvider value={{ packageMeta }}>
<Developers type={type} visibleMax={1} />
<Developers type={DeveloperType.CONTRIBUTORS} visibleMax={1} />
</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 styled from '@emotion/styled';
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 {
type: DevelopersType;
type: DeveloperType;
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;
const Developers: FC<Props> = ({ type, visibleMax }) => {
const [visibleDevs, setVisibleDevs] = React.useState<number>(visibleMax || VISIBLE_MAX);
const { packageMeta } = React.useContext(DetailContext);
const Developers: React.FC<Props> = ({ type, visibleMax = VISIBLE_MAX }) => {
const detailContext = useContext(DetailContext);
const handleLoadMore = (): void => {
setVisibleDevs(visibleDevs + VISIBLE_MAX);
};
const renderDeveloperDetails = ({ name, avatar, email }, packageMeta): JSX.Element => {
const { name: packageName, version } = packageMeta.latest;
return <AvatarTooltip avatar={avatar} email={email} name={name} packageName={packageName} version={version} />;
};
const renderDevelopers = (developers, packageMeta): JSX.Element => {
const listVisibleDevelopers = developers.slice(0, visibleDevs);
return (
<Fragment>
<StyledText variant={'subtitle1'}>{type}</StyledText>
<Content>
{listVisibleDevelopers.map(developer => (
<Details key={developer.email}>{renderDeveloperDetails(developer, packageMeta)}</Details>
))}
{visibleDevs < developers.length && (
<Fab onClick={handleLoadMore} size="small">
<Add />
</Fab>
)}
</Content>
</Fragment>
);
};
const developerList = packageMeta && packageMeta.latest[type];
if (!developerList || developerList.length === 0) {
return null;
if (!detailContext) {
throw Error("The app's detail Context was not correct used");
}
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;

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 { fontWeight } from '../../utils/styles/sizes';
import Text from '../../muiComponents/Text';
import FloatingActionButton from '../../muiComponents/FloatingActionButton';
import { Theme } from '../../design-tokens/theme';
@@ -20,11 +19,11 @@ export const Content = styled('div')({
},
});
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
marginBottom: '10px',
textTransform: 'capitalize',
});
}));
export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>(props => ({
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 }) =>
children ? (
<DistChips
// lint rule conflicting with prettier
/* eslint-disable react/jsx-wrap-multilines */
label={
<>
<b>{name}</b>
@@ -19,7 +17,6 @@ const DistChip: FC<{ name: string }> = ({ name, children }) =>
{children}
</>
}
/* eslint-enable */
/>
) : null;

View File

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

View File

@@ -19,7 +19,6 @@ const Engine: React.FC = () => {
return null;
}
/* eslint-disable react/jsx-max-depth */
return (
<Grid container={true}>
{engines.node && (
@@ -45,7 +44,6 @@ const Engine: React.FC = () => {
)}
</Grid>
);
/* eslint-enable react/jsx-max-depth */
};
export default Engine;

View File

@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Engines /> component should render the component in default state 1`] = `"<div class=\\"MuiGrid-root MuiGrid-container\\"><div class=\\"MuiGrid-root MuiGrid-item MuiGrid-grid-xs-6\\"><ul class=\\"MuiList-root MuiList-padding MuiList-subheader\\"><h6 class=\\"MuiTypography-root css-1na337r-StyledText et66bt70 MuiTypography-subtitle1\\">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 { fontWeight } from '../../utils/styles/sizes';
import ListItem from '../../muiComponents/ListItem';
import Text from '../../muiComponents/Text';
import { Theme } from '../../design-tokens/theme';
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
textTransform: 'capitalize',
});
}));
export const EngineListItem = styled(ListItem)({
paddingLeft: 0,

View File

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

View File

@@ -18,7 +18,7 @@ describe('<Header /> component with logged in state', () => {
test('should load the component in logged out state', () => {
const { container, queryByTestId, getByText } = render(
<Router>
<AppContextProvider packages={props.packages}>
<AppContextProvider>
<Header />
</AppContextProvider>
</Router>
@@ -32,7 +32,7 @@ describe('<Header /> component with logged in state', () => {
test('should load the component in logged in state', () => {
const { container, getByTestId, queryByText } = render(
<Router>
<AppContextProvider packages={props.packages} user={props.user}>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
@@ -44,9 +44,9 @@ describe('<Header /> component with logged in state', () => {
});
test('should open login dialog', async () => {
const { getByText, getByTestId } = render(
const { getByText } = render(
<Router>
<AppContextProvider packages={props.packages}>
<AppContextProvider>
<Header />
</AppContextProvider>
</Router>
@@ -54,15 +54,14 @@ describe('<Header /> component with logged in state', () => {
const loginBtn = getByText('Login');
fireEvent.click(loginBtn);
// wait for login modal appearance and return the element
const registrationInfoModal = await waitForElement(() => getByTestId('login--form-container'));
expect(registrationInfoModal).toBeTruthy();
const loginDialog = await waitForElement(() => getByText('Sign in'));
expect(loginDialog).toBeTruthy();
});
test('should logout the user', async () => {
const { getByText, getByTestId } = render(
<Router>
<AppContextProvider packages={props.packages} user={props.user}>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
@@ -80,7 +79,7 @@ describe('<Header /> component with logged in state', () => {
test("The question icon should open a new tab of verdaccio's website - installation doc", () => {
const { getByTestId } = render(
<Router>
<AppContextProvider packages={props.packages} user={props.user}>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
@@ -93,7 +92,7 @@ describe('<Header /> component with logged in state', () => {
test('should open the registrationInfo modal when clicking on the info icon', async () => {
const { getByTestId } = render(
<Router>
<AppContextProvider packages={props.packages} user={props.user}>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>
@@ -110,7 +109,7 @@ describe('<Header /> component with logged in state', () => {
test('should close the registrationInfo modal when clicking on the button close', async () => {
const { getByTestId, getByText, queryByTestId } = render(
<Router>
<AppContextProvider packages={props.packages} user={props.user}>
<AppContextProvider user={props.user}>
<Header />
</AppContextProvider>
</Router>

View File

@@ -2,10 +2,9 @@ import React, { useState, useContext } from 'react';
import storage from '../../utils/storage';
import { getRegistryURL } from '../../utils/url';
import { makeLogin } from '../../utils/login';
import Button from '../../muiComponents/Button';
import AppContext from '../../App/AppContext';
import LoginModal from '../Login';
import LoginDialog from '../LoginDialog';
import Search from '../Search';
import { NavBar, InnerNavBar, MobileNavBar, InnerMobileNavBar } from './styles';
@@ -17,9 +16,7 @@ interface Props {
withoutSearch?: boolean;
}
/* eslint-disable react/jsx-max-depth */
/* eslint-disable react/jsx-no-bind*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
const Header: React.FC<Props> = ({ withoutSearch }) => {
const appContext = useContext(AppContext);
const [isInfoDialogOpen, setOpenInfoDialog] = useState();
@@ -30,29 +27,9 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
throw Error('The app Context was not correct used');
}
const { user, scope, error, setUser, setError } = appContext;
const { user, scope, setUser } = appContext;
const logo = window.VERDACCIO_LOGO;
/**
* handles login
* Required by: <Header />
*/
const 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);
setUser({ username });
setShowLoginModal(false);
}
if (error) {
setUser(undefined);
setError(error);
}
};
/**
* Logouts user
* Required by: <Header />
@@ -94,12 +71,7 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
</Button>
</MobileNavBar>
)}
<LoginModal
error={error}
onCancel={() => setShowLoginModal(false)}
onSubmit={handleDoLogin}
visibility={showLoginModal}
/>
{!user && <LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />}
</>
);
};

View File

@@ -1,4 +1,5 @@
import React from 'react';
import styled from '@emotion/styled';
import Logo from '../Logo';
@@ -8,7 +9,14 @@ interface Props {
const HeaderLogo: React.FC<Props> = ({ 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 />;

View File

@@ -16,7 +16,6 @@ interface Props {
onLoggedInMenuClose: () => void;
}
/* eslint-disable react/jsx-max-depth */
const HeaderMenu: React.FC<Props> = ({
onLogout,
username,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,20 +7,26 @@ interface Props extends Pick<TextProps, 'variant'> {
external?: boolean;
className?: string;
to: string;
children?: React.ReactNode;
}
type LinkRef = HTMLAnchorElement;
/* 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>;
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}
</a>
) : (
<RouterLink className={className} to={to} {...props}>
<RouterLink className={className} innerRef={ref} to={to} {...props}>
{LinkTextContent}
</RouterLink>
);
};
});
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,254 +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
data-testid={'login--form-container'}
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\\" data-testid=\\"login--form-container\\" 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\\" data-testid=\\"login--form-container\\" 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,105 @@
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: '',
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 Button from '../../muiComponents/Button';
import Heading from '../../muiComponents/Heading';
import { spacings } from '../../utils/styles/spacings';
import { Theme } from '../../design-tokens/theme';
import PackageImg from './img/package.svg';
@@ -21,7 +20,7 @@ const EmptyPackage = styled('img')({
const StyledHeading = styled(Heading)<{ theme?: Theme }>(props => ({
color: props.theme && props.theme.palette.primary.main,
marginBottom: spacings.sm,
marginBottom: 16,
}));
const NotFound: React.FC = () => {

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import React, { Fragment, ReactNode } from 'react';
import styled from '@emotion/styled';
import Package from '../Package';
import Help from '../Help';
@@ -6,7 +7,10 @@ import { formatLicense } from '../../utils/package';
import { PackageInterface } from '../Package/Package';
import Divider from '../../muiComponents/Divider';
import * as classes from './styles';
const PkgContainer = styled('div')({
margin: 0,
padding: 0,
});
interface Props {
packages: PackageInterface[];
@@ -32,7 +36,7 @@ export const PackageList: React.FC<Props> = ({ packages }) => {
return (
<div className={'package-list-items'}>
<div className={classes.pkgContainer}>{hasPackages() ? renderPackages() : <Help />}</div>
<PkgContainer>{hasPackages() ? renderPackages() : <Help />}</PkgContainer>
</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 { fontSize } from '../../utils/styles/sizes';
import DialogTitle from '../../muiComponents/DialogTitle';
import DialogContent from '../../muiComponents/DialogContent';
import { Theme } from '../../design-tokens/theme';
@@ -8,7 +7,7 @@ import { Theme } from '../../design-tokens/theme';
export const Title = styled(DialogTitle)<{ theme?: Theme }>(props => ({
backgroundColor: props.theme && props.theme.palette.primary.main,
color: props.theme && props.theme.palette.white,
fontSize: fontSize.lg,
fontSize: props.theme && props.theme.fontSize.lg,
}));
export const Content = styled(DialogContent)({

View File

@@ -1,64 +1,58 @@
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 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(() => ({
latest: {
homepage: 'https://verdaccio.tld',
bugs: {
url: 'https://verdaccio.tld/bugs',
},
dist: {
tarball: 'https://verdaccio.tld/download',
},
},
}));
jest.mock('../../pages/Version', () => ({
DetailContextConsumer: component => {
return component.children({ packageMeta: mockPackageMeta() });
},
}));
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({ contextValue }) => (
<DetailContext.Provider value={contextValue}>
<Repository />
</DetailContext.Provider>
);
describe('<Repository /> component', () => {
beforeEach(() => {
jest.resetAllMocks();
});
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 load the component in default state', () => {
const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(container.firstChild).toMatchSnapshot();
});
test('should render the component in with no repository data', () => {
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(wrapper.html()).toEqual('');
expect(queryByText('Repository')).toBeFalsy();
});
test('should render the component in with invalid url', () => {
const packageMeta = {
...detailContextValue.packageMeta,
latest: {
...detailContextValue.packageMeta?.latest,
repository: {
type: '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(wrapper.html()).toEqual('');
expect(queryByText('Repository')).toBeFalsy();
});
});

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,32 +1,12 @@
import React, { KeyboardEvent, Component, ReactElement } 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 React, { useState, FormEvent, useCallback } from 'react';
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 { callSearch } from '../../utils/calls';
import { Theme } from '../../design-tokens/theme';
export interface State {
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;
import SearchAdornment from './SearchAdornment';
const CONSTANTS = {
API_DELAY: 300,
@@ -34,168 +14,142 @@ const CONSTANTS = {
ABORT_ERROR: 'AbortError',
};
const StyledInputAdornment = styled(InputAdornment)<{ theme?: Theme }>(props => ({
color: props.theme && props.theme.palette.white,
}));
export class Search extends Component<RouteComponentProps<{}>, State> {
constructor(props: RouteComponentProps<{}>) {
super(props);
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}
/>
);
}
const Search: React.FC<RouteComponentProps> = ({ history }) => {
const [suggestions, setSuggestions] = useState([]);
const [loaded, setLoaded] = useState(false);
const [search, setSearch] = useState('');
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [requestList, setRequestList] = useState<Array<{ abort: () => void }>>([]);
/**
* Cancel all the requests which are in pending state.
*/
private cancelAllSearchRequests: cancelAllSearchRequests = () => {
this.requestList.forEach(request => request.abort());
this.requestList = [];
};
/**
* 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>
);
}
const cancelAllSearchRequests = useCallback(() => {
requestList.forEach(request => request.abort());
setRequestList([]);
}, [requestList, setRequestList]);
/**
* As user focuses out from input, we cancel all the request from requestList
* and set the API state parameters to default boolean values.
*/
private handleOnBlur: onBlur = event => {
// stops event bubbling
event.stopPropagation();
this.setState(
{
loaded: false,
loading: false,
error: false,
},
() => this.cancelAllSearchRequests()
);
};
}
const handleOnBlur = useCallback(
(event: FormEvent<HTMLInputElement>) => {
// stops event bubbling
event.stopPropagation();
setLoaded(false);
setLoading(false);
setError(false);
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);

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
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}>
<ListItemText>{name}</ListItemText>
<Spacer />
<ListItemText>{`${formatDateDistance(uplinks[name].fetched)} ago`}</ListItemText>
<ListItemText>{formatDateDistance(uplinks[name].fetched)}</ListItemText>
</ListItem>
))}
</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 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 { default as MuiListItemText } from '../../muiComponents/ListItemText';
import { fontWeight } from '../../utils/styles/sizes';
import { Theme } from '../../design-tokens/theme';
export const StyledText = styled(Text)({
fontWeight: fontWeight.bold,
});
export const StyledText = styled(Text)<{ theme?: Theme }>(props => ({
fontWeight: props.theme && props.theme.fontWeight.bold,
}));
export const Spacer = styled('div')({
flex: '1 1 auto',

View File

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

View File

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

View File

@@ -1,17 +1,16 @@
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import { fontWeight } from '../utils/styles/sizes';
import { breakpoints } from '../utils/styles/media';
import { Theme } from './theme';
const resetStyles = makeStyles(() => ({
const resetStyles = makeStyles(({ theme }: { theme?: Theme }) => ({
'@global': {
// 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': {
fontFamily: '"Roboto", Helvetica Neue, Arial, sans-serif',
},
strong: {
fontWeight: fontWeight.semiBold,
fontWeight: theme && theme.fontWeight.semiBold,
},
'html, body, #root': {
height: '100%',
@@ -25,8 +24,8 @@ const resetStyles = makeStyles(() => ({
padding: 15,
flex: 1,
[`@media screen and (min-width: ${breakpoints.container}px)`]: {
maxWidth: breakpoints.container,
[`@media screen and (min-width: ${theme && theme.breakPoints.container}px)`]: {
maxWidth: theme && theme.breakPoints.container,
width: '100%',
marginLeft: 'auto',
marginRight: 'auto',

View File

@@ -6,6 +6,7 @@ const colors = {
black: '#000',
white: '#fff',
red: '#d32f2f',
orange: '#CD4000',
greySuperLight: '#f5f5f5',
greyLight: '#d3d3d3',
greyLight2: '#908ba1',
@@ -28,6 +29,44 @@ const 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({
typography: {
fontFamily: 'inherit',
@@ -38,15 +77,24 @@ export const theme = createMuiTheme({
secondary: { main: colors.secondary },
error: { main: colors.red },
},
...customizedTheme,
});
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' {
interface CustomPalette {
black: string;
white: string;
red: string;
orange: string;
greySuperLight: string;
greyLight: string;
greyLight2: string;

View File

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

View File

@@ -1,13 +1,10 @@
import React, { forwardRef } from 'react';
import { default as MaterialUITypography, TypographyProps } from '@material-ui/core/Typography';
import { default as MaterialUITypography } from '@material-ui/core/Typography';
import { TextProps } from './TextConfig';
type TextType = 'subtitle1' | 'subtitle2' | 'body1' | 'body2';
type TextRef = HTMLElementTagNameMap[keyof HTMLElementTagNameMap];
export interface TextProps extends Omit<TypographyProps, 'variant'> {
variant?: TextType;
}
// The reference is already from type of the Component, so the any below is not a problem
const Text = forwardRef<TextRef, TextProps>(function Text(props, ref) {
return <MaterialUITypography {...props} ref={ref} />;

View File

@@ -0,0 +1,7 @@
import { TypographyProps } from '@material-ui/core/Typography';
type TextType = 'subtitle1' | 'subtitle2' | 'body1' | 'body2';
export interface TextProps extends Omit<TypographyProps, 'variant'> {
variant?: TextType;
}

View File

@@ -1 +1,2 @@
export { default, TextProps } from './Text';
export { default } from './Text';
export { TextProps } from './TextConfig';

View File

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

View File

@@ -1,24 +1,6 @@
import { createContext, Consumer, Provider } from 'react';
import { PackageMetaInterface } from '../../../types/packageMeta';
export interface DetailContextProps {
packageMeta: PackageMetaInterface;
packageVersion?: string;
readMe: string;
packageName: string;
enableLoading: () => void;
isLoading: boolean;
hasNotBeenFound: boolean;
}
export interface VersionPageConsumerProps {
packageMeta: PackageMetaInterface;
readMe: string;
packageName: string;
packageVersion?: string;
// FIXME: looking for the appropiated type here
enableLoading: any;
}
import { DetailContextProps, VersionPageConsumerProps } from './version-config';
export const DetailContext = createContext<Partial<DetailContextProps>>({});

View File

@@ -1,8 +1,4 @@
export {
DetailContext,
DetailContextConsumer,
DetailContextProvider,
DetailContextProps,
VersionPageConsumerProps,
} from './context';
export { DetailContext, DetailContextConsumer, DetailContextProvider } from './context';
export { VersionPageConsumerProps, DetailContextProps } from './version-config';
export { default } from './Version';

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