mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-16 21:56:25 -05:00
refactor: rematch as web state storage for UI (#2447)
* trying rematch refactor: rematch for store packages migrate login to rematch Update packages/plugins/ui-theme/src/store/store.ts Co-authored-by: Sergio Moreno <sergiomorenoalbert@gmail.com> hide temporary fix test for login migrate package download resource fix tests * add missing fixture * migrate detail page support * fix tests * migrate search * migrate search * clean up tests * remove tags * fix lint * add changeset * fix: search model typings * add type * types * apply suggestions Co-authored-by: Sergio Moreno <22656541+semoal@users.noreply.github.com>
This commit is contained in:
parent
9dbf73e955
commit
5fed1955a9
60 changed files with 1135 additions and 945 deletions
5
.changeset/olive-candles-speak.md
Normal file
5
.changeset/olive-candles-speak.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@verdaccio/ui-theme': minor
|
||||
---
|
||||
|
||||
feat: integrate rematch for ui state storage
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -48,5 +48,5 @@ hyper-results*.json
|
|||
api-results*.json
|
||||
|
||||
#docs
|
||||
api/
|
||||
./api
|
||||
packages/core/core/docs
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
"jest-environment-jsdom": "27.1.0",
|
||||
"jest-environment-jsdom-global": "3.0.0",
|
||||
"jest-environment-node": "27.1.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"jest-junit": "12.2.0",
|
||||
"kleur": "3.0.3",
|
||||
"lint-staged": "11.1.2",
|
||||
|
@ -123,7 +122,7 @@
|
|||
"benchmark:submit": "pnpm ts-node ./scripts/submit-metrics.ts",
|
||||
"start:watch": "concurrently --kill-others \"pnpm _build:watch\" \"pnpm _start:server\" \"pnpm _debug:reload\"",
|
||||
"_build:watch": "pnpm run --parallel watch --filter ./packages",
|
||||
"_start:server": "node packages/verdaccio/debug/bootstrap.js --listen 8000",
|
||||
"_start:server": "node --inspect packages/verdaccio/debug/bootstrap.js --listen 8000",
|
||||
"_start:web": "pnpm start --filter ...@verdaccio/ui-theme",
|
||||
"_debug:reload": "nodemon -d 3 packages/verdaccio/debug/bootstrap.js",
|
||||
"start:ts": "ts-node packages/verdaccio/src/start.ts -- --listen 8000",
|
||||
|
@ -138,7 +137,7 @@
|
|||
"ts:ref": "update-ts-references --discardComments",
|
||||
"website": "pnpm build --filter ...@verdaccio/website",
|
||||
"crowdin:upload": "crowdin upload sources --auto-update --config ./crowdin.yaml",
|
||||
"crowdin:download": "crowdin download --config ./crowdin.yaml",
|
||||
"crowdin:download": "crowdin download --verbose --config ./crowdin.yaml",
|
||||
"crowdin:sync": "pnpm crowdin:upload && pnpm crowdin:download --verbose",
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
|
|
42
packages/plugins/ui-theme/jest/api/packages.json
Normal file
42
packages/plugins/ui-theme/jest/api/packages.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
[
|
||||
{
|
||||
"name": "test",
|
||||
"version": "1.0.22",
|
||||
"description": "test",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "http",
|
||||
"url": "git+https://github.com/test/test.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": {
|
||||
"name": "",
|
||||
"email": "",
|
||||
"url": "",
|
||||
"avatar": "data:image/svg+xml;utf8,%3Csvg%20height%3D%22100%22%20viewBox%3D%22-27%2024%20100%20100%22%20width%3D%22100%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Ccircle%20cx%3D%2223%22%20cy%3D%2274%22%20id%3D%22a%22%20r%3D%2250%22%2F%3E%3C%2Fdefs%3E%3Cuse%20fill%3D%22%23F5EEE5%22%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23a%22%2F%3E%3CclipPath%20id%3D%22b%22%3E%3Cuse%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23a%22%2F%3E%3C%2FclipPath%3E%3Cg%20clip-path%3D%22url(%23b)%22%3E%3Cdefs%3E%3Cpath%20d%3D%22M36%2095.9c0%204%204.7%205.2%207.1%205.8%207.6%202%2022.8%205.9%2022.8%205.9%203.2%201.1%205.7%203.5%207.1%206.6v9.8H-27v-9.8c1.3-3.1%203.9-5.5%207.1-6.6%200%200%2015.2-3.9%2022.8-5.9%202.4-.6%207.1-1.8%207.1-5.8V85h26v10.9z%22%20id%3D%22c%22%2F%3E%3C%2Fdefs%3E%3Cuse%20fill%3D%22%23E6C19C%22%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23c%22%2F%3E%3CclipPath%20id%3D%22d%22%3E%3Cuse%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23c%22%2F%3E%3C%2FclipPath%3E%3Cpath%20clip-path%3D%22url(%23d)%22%20d%3D%22M23.2%2035h.2c3.3%200%208.2.2%2011.4%202%203.3%201.9%207.3%205.6%208.5%2012.1%202.4%2013.7-2.1%2035.4-6.3%2042.4-4%206.7-9.8%209.2-13.5%209.4H23h-.1c-3.7-.2-9.5-2.7-13.5-9.4-4.2-7-8.7-28.7-6.3-42.4%201.2-6.5%205.2-10.2%208.5-12.1%203.2-1.8%208.1-2%2011.4-2h.2z%22%20fill%3D%22%23D4B08C%22%2F%3E%3C%2Fg%3E%3Cpath%20d%3D%22M22.6%2040c19.1%200%2020.7%2013.8%2020.8%2015.1%201.1%2011.9-3%2028.1-6.8%2033.7-4%205.9-9.8%208.1-13.5%208.3h-.5c-3.8-.3-9.6-2.5-13.6-8.4-3.8-5.6-7.9-21.8-6.8-33.8C2.3%2053.7%203.5%2040%2022.6%2040z%22%20fill%3D%22%23F2CEA5%22%2F%3E%3C%2Fsvg%3E"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"readmeFilename": "README.md",
|
||||
"bugs": {
|
||||
"url": "https://github.com/test/test/issues"
|
||||
},
|
||||
"homepage": "https://github.com/test/test#readme",
|
||||
"_id": "test@1.0.22",
|
||||
"_nodeVersion": "14.17.4",
|
||||
"_npmVersion": "7.20.5",
|
||||
"dist": {
|
||||
"integrity": "sha512-2IDD0lLzGUL7YJ+17Oh9VtbOwdKLqBLS+ZFATDXi5R22TL2hZ9LBFE10bzsDovNc4xtgwZAk1/K+5LHTye4ztg==",
|
||||
"shasum": "c9152f57636bce762ccb5a83113c42a5831579bc",
|
||||
"tarball": "http://localhost:4873/test/-/test-1.0.22.tgz"
|
||||
},
|
||||
"contributors": [],
|
||||
"time": "2021-08-14T20:15:19.336Z",
|
||||
"users": {}
|
||||
}
|
||||
]
|
|
@ -10,9 +10,9 @@ module.exports = Object.assign({}, config, {
|
|||
'^.+\\.(js|ts|tsx)$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['js', 'ts', 'tsx'],
|
||||
testURL: 'http://localhost',
|
||||
testURL: 'http://localhost:9000/',
|
||||
rootDir: '..',
|
||||
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
|
||||
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '<rootDir>/jest/setup-env.ts'],
|
||||
setupFiles: ['<rootDir>/jest/setup.ts'],
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/(?!react-syntax-highlighter)'],
|
||||
modulePathIgnorePatterns: [
|
||||
|
|
9
packages/plugins/ui-theme/jest/server-handlers.ts
Normal file
9
packages/plugins/ui-theme/jest/server-handlers.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { rest } from 'msw';
|
||||
|
||||
const packagesPayload = require('./api/packages.json');
|
||||
|
||||
export const handlers = [
|
||||
rest.get('http://localhost:9000/-/verdaccio/packages', (req, res, ctx) => {
|
||||
return res(ctx.json(packagesPayload));
|
||||
}),
|
||||
];
|
6
packages/plugins/ui-theme/jest/server.ts
Normal file
6
packages/plugins/ui-theme/jest/server.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { setupServer } from 'msw/node';
|
||||
|
||||
import { handlers } from './server-handlers';
|
||||
|
||||
const server = setupServer(...handlers);
|
||||
export { server };
|
15
packages/plugins/ui-theme/jest/setup-env.ts
Normal file
15
packages/plugins/ui-theme/jest/setup-env.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import '@testing-library/jest-dom/extend-expect';
|
||||
import 'whatwg-fetch';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { server } from './server';
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen({
|
||||
onUnhandledRequest: 'warn',
|
||||
});
|
||||
});
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
|
@ -2,29 +2,24 @@
|
|||
* Setup configuration for Jest
|
||||
* This file includes global settings for the JEST environment.
|
||||
*/
|
||||
import { GlobalWithFetchMock } from 'jest-fetch-mock';
|
||||
import 'mutationobserver-shim';
|
||||
|
||||
// @ts-ignore : Property '__VERDACCIO_BASENAME_UI_OPTIONS' does not exist on type 'Global'.
|
||||
global.__VERDACCIO_BASENAME_UI_OPTIONS = {
|
||||
base: 'http://localhost',
|
||||
base: 'http://localhost:9000/',
|
||||
protocol: 'http',
|
||||
host: 'localhost',
|
||||
primaryColor: '#4b5e40',
|
||||
url_prefix: '',
|
||||
darkMode: false,
|
||||
language: 'en-US',
|
||||
uri: 'http://localhost:4873',
|
||||
uri: 'http://localhost:9000/',
|
||||
pkgManagers: ['pnpm', 'yarn', 'npm'],
|
||||
title: 'Verdaccio Dev UI',
|
||||
scope: '',
|
||||
version: 'v1.0.0',
|
||||
};
|
||||
|
||||
const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;
|
||||
customGlobal.fetch = require('jest-fetch-mock');
|
||||
customGlobal.fetchMock = customGlobal.fetch;
|
||||
|
||||
// mocking few DOM methods
|
||||
// @ts-ignore : Property 'document' does not exist on type 'Global'.
|
||||
if (global.document) {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"@types/react-autosuggest": "10.1.5",
|
||||
"@types/react-dom": "17.0.9",
|
||||
"@types/react-helmet": "6.1.2",
|
||||
"@types/redux": "3.6.0",
|
||||
"@types/react-router-dom": "5.1.8",
|
||||
"@types/react-virtualized": "9.21.13",
|
||||
"@emotion/core": "10.1.1",
|
||||
|
@ -30,9 +31,11 @@
|
|||
"@material-ui/core": "4.11.4",
|
||||
"@material-ui/icons": "4.11.2",
|
||||
"@material-ui/styles": "4.11.4",
|
||||
"@testing-library/dom": "8.2.0",
|
||||
"@rematch/core": "2.1.0",
|
||||
"@rematch/loading": "2.1.0",
|
||||
"@testing-library/dom": "8.5.0",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "12.0.0",
|
||||
"@testing-library/react": "12.1.0",
|
||||
"@verdaccio/node-api": "workspace:6.0.0-6-next.20",
|
||||
"autosuggest-highlight": "3.1.1",
|
||||
"babel-loader": "8.2.2",
|
||||
|
@ -65,13 +68,16 @@
|
|||
"react": "17.0.2",
|
||||
"react-autosuggest": "10.1.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-hook-form": "7.14.2",
|
||||
"react-hook-form": "7.15.3",
|
||||
"react-hot-loader": "4.13.0",
|
||||
"react-i18next": "11.12.0",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"react-virtualized": "9.22.3",
|
||||
"react-redux": "7.2.1",
|
||||
"redux": "4.1.1",
|
||||
"rimraf": "3.0.2",
|
||||
"msw": "0.35.0",
|
||||
"standard-version": "9.3.1",
|
||||
"style-loader": "3.2.1",
|
||||
"stylelint": "13.13.1",
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import storage from 'verdaccio-ui/utils/storage';
|
||||
import { render, waitFor, fireEvent } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
import {
|
||||
renderWithStore,
|
||||
act,
|
||||
waitFor,
|
||||
fireEvent,
|
||||
screen,
|
||||
} from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
|
||||
// eslint-disable-next-line jest/no-mocks-import
|
||||
import { generateTokenWithTimeRange } from '../../jest/unit/components/__mocks__/token';
|
||||
import { store } from '../store';
|
||||
|
||||
import App from './App';
|
||||
|
||||
|
@ -30,68 +36,88 @@ jest.mock('verdaccio-ui/utils/storage', () => {
|
|||
return new LocalStorageMock();
|
||||
});
|
||||
|
||||
jest.mock('verdaccio-ui/providers/API/api', () => ({
|
||||
// eslint-disable-next-line jest/no-mocks-import
|
||||
request: require('../../jest/unit/components/__mocks__/api').default.request,
|
||||
}));
|
||||
// force the windows to expand to display items
|
||||
// https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107
|
||||
jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600);
|
||||
jest.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(600);
|
||||
|
||||
/* eslint-disable react/jsx-no-bind*/
|
||||
describe('<App />', () => {
|
||||
// test('should display the Header component ', async () => {
|
||||
// const { queryByTestId } = render(<App />);
|
||||
//
|
||||
// expect(queryByTestId('loading')).toBeTruthy();
|
||||
//
|
||||
// // wait for the Header component appearance and return the element
|
||||
// const headerElement = await waitFor(() => queryByTestId('header'));
|
||||
// expect(headerElement).toBeTruthy();
|
||||
// });
|
||||
describe('login - log out', () => {
|
||||
test('handleLogout - logouts the user and clear localstorage', async () => {
|
||||
const { queryByTestId } = renderWithStore(<App />, store);
|
||||
store.dispatch.login.logInUser({
|
||||
username: 'verdaccio',
|
||||
token: generateTokenWithTimeRange(24),
|
||||
});
|
||||
|
||||
test('handleLogout - logouts the user and clear localstorage', async () => {
|
||||
storage.setItem('username', 'verdaccio');
|
||||
storage.setItem('token', generateTokenWithTimeRange(24));
|
||||
// wait for the Account's circle element component appearance and return the element
|
||||
const accountCircleElement = await waitFor(() => queryByTestId('logInDialogIcon'));
|
||||
expect(accountCircleElement).toBeTruthy();
|
||||
|
||||
const { queryByTestId } = render(<App />);
|
||||
if (accountCircleElement) {
|
||||
fireEvent.click(accountCircleElement);
|
||||
|
||||
// wait for the Account's circle element component appearance and return the element
|
||||
const accountCircleElement = await waitFor(() => queryByTestId('header--menu-accountcircle'));
|
||||
expect(accountCircleElement).toBeTruthy();
|
||||
// wait for the Button's logout element component appearance and return the element
|
||||
const buttonLogoutElement = await waitFor(() => queryByTestId('logOutDialogIcon'));
|
||||
expect(buttonLogoutElement).toBeTruthy();
|
||||
|
||||
if (accountCircleElement) {
|
||||
fireEvent.click(accountCircleElement);
|
||||
if (buttonLogoutElement) {
|
||||
fireEvent.click(buttonLogoutElement);
|
||||
|
||||
// wait for the Button's logout element component appearance and return the element
|
||||
const buttonLogoutElement = await waitFor(() => queryByTestId('header--button-logout'));
|
||||
expect(buttonLogoutElement).toBeTruthy();
|
||||
|
||||
if (buttonLogoutElement) {
|
||||
fireEvent.click(buttonLogoutElement);
|
||||
|
||||
expect(queryByTestId('greetings-label')).toBeFalsy();
|
||||
expect(queryByTestId('greetings-label')).toBeFalsy();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('isUserAlreadyLoggedIn: token already available in storage', async () => {
|
||||
const { queryByTestId, queryAllByText } = renderWithStore(<App />, store);
|
||||
store.dispatch.login.logInUser({
|
||||
username: 'verdaccio',
|
||||
token: generateTokenWithTimeRange(24),
|
||||
});
|
||||
|
||||
// wait for the Account's circle element component appearance and return the element
|
||||
const accountCircleElement = await waitFor(() => queryByTestId('logInDialogIcon'));
|
||||
expect(accountCircleElement).toBeTruthy();
|
||||
|
||||
if (accountCircleElement) {
|
||||
fireEvent.click(accountCircleElement);
|
||||
|
||||
// wait for the Greeting's label element component appearance and return the element
|
||||
const greetingsLabelElement = await waitFor(() => queryByTestId('greetings-label'));
|
||||
expect(greetingsLabelElement).toBeTruthy();
|
||||
|
||||
if (greetingsLabelElement) {
|
||||
expect(queryAllByText('verdaccio')).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('isUserAlreadyLoggedIn: token already available in storage', async () => {
|
||||
storage.setItem('username', 'verdaccio');
|
||||
storage.setItem('token', generateTokenWithTimeRange(24));
|
||||
describe('list packages', () => {
|
||||
test('should display the Header component', async () => {
|
||||
renderWithStore(<App />, store);
|
||||
|
||||
const { queryByTestId, queryAllByText } = render(<App />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('loading')).toBeTruthy();
|
||||
});
|
||||
|
||||
// wait for the Account's circle element component appearance and return the element
|
||||
const accountCircleElement = await waitFor(() => queryByTestId('header--menu-accountcircle'));
|
||||
expect(accountCircleElement).toBeTruthy();
|
||||
// wait for the Header component appearance and return the element
|
||||
const headerElement = await waitFor(() => screen.queryByTestId('header'));
|
||||
expect(headerElement).toBeTruthy();
|
||||
});
|
||||
|
||||
if (accountCircleElement) {
|
||||
fireEvent.click(accountCircleElement);
|
||||
test('should display package lists', async () => {
|
||||
act(() => {
|
||||
renderWithStore(<App />, store);
|
||||
});
|
||||
|
||||
// wait for the Greeting's label element component appearance and return the element
|
||||
const greetingsLabelElement = await waitFor(() => queryByTestId('greetings-label'));
|
||||
expect(greetingsLabelElement).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('package-item-list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
if (greetingsLabelElement) {
|
||||
expect(queryAllByText('verdaccio')).toBeTruthy();
|
||||
}
|
||||
}
|
||||
expect(store.getState().packages.response).toHaveLength(1);
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable react/jsx-max-depth */
|
||||
import styled from '@emotion/styled';
|
||||
import isNil from 'lodash/isNil';
|
||||
import React, { useState, useEffect, Suspense } from 'react';
|
||||
import React, { useEffect, Suspense } from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import Box from 'verdaccio-ui/components/Box';
|
||||
|
@ -9,10 +8,7 @@ import Loading from 'verdaccio-ui/components/Loading';
|
|||
import loadDayJSLocale from 'verdaccio-ui/design-tokens/load-dayjs-locale';
|
||||
import StyleBaseline from 'verdaccio-ui/design-tokens/StyleBaseline';
|
||||
import { Theme } from 'verdaccio-ui/design-tokens/theme';
|
||||
import { isTokenExpire } from 'verdaccio-ui/utils/login';
|
||||
import storage from 'verdaccio-ui/utils/storage';
|
||||
|
||||
import AppContextProvider from './AppContextProvider';
|
||||
import AppRoute, { history } from './AppRoute';
|
||||
import Footer from './Footer';
|
||||
import Header from './Header';
|
||||
|
@ -32,35 +28,8 @@ const StyledBoxContent = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
const App: React.FC = () => {
|
||||
const [user, setUser] = useState<undefined | { username: string | null }>();
|
||||
/**
|
||||
* Logout user
|
||||
* Required by: <Header />
|
||||
*/
|
||||
const logout = () => {
|
||||
storage.removeItem('username');
|
||||
storage.removeItem('token');
|
||||
setUser(undefined);
|
||||
};
|
||||
|
||||
const checkUserAlreadyLoggedIn = () => {
|
||||
// checks for token validity
|
||||
const token = storage.getItem('token');
|
||||
const username = storage.getItem('username');
|
||||
|
||||
if (isTokenExpire(token) || isNil(username)) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
|
||||
setUser({ username });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkUserAlreadyLoggedIn();
|
||||
loadDayJSLocale();
|
||||
}, []);
|
||||
|
||||
|
@ -70,12 +39,10 @@ const App: React.FC = () => {
|
|||
<StyledBox display="flex" flexDirection="column" height="100%">
|
||||
<>
|
||||
<Router history={history}>
|
||||
<AppContextProvider user={user}>
|
||||
<Header />
|
||||
<StyledBoxContent flexGrow={1}>
|
||||
<AppRoute />
|
||||
</StyledBoxContent>
|
||||
</AppContextProvider>
|
||||
<Header />
|
||||
<StyledBoxContent flexGrow={1}>
|
||||
<AppRoute />
|
||||
</StyledBoxContent>
|
||||
</Router>
|
||||
<Footer />
|
||||
</>
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import { createContext } from 'react';
|
||||
|
||||
export interface AppProps {
|
||||
user?: User;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
username: string | null;
|
||||
}
|
||||
|
||||
export interface AppContextProps extends AppProps {
|
||||
setUser: (user?: User) => void;
|
||||
}
|
||||
|
||||
const AppContext = createContext<undefined | AppContextProps>(undefined);
|
||||
|
||||
export default AppContext;
|
|
@ -1,44 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { useConfig } from 'verdaccio-ui/providers/config';
|
||||
|
||||
import AppContext, { AppProps, User } from './AppContext';
|
||||
|
||||
interface Props {
|
||||
user?: User;
|
||||
}
|
||||
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
const AppContextProvider: React.FC<Props> = ({ children, user }) => {
|
||||
const { configOptions } = useConfig();
|
||||
const [state, setState] = useState<AppProps>({
|
||||
scope: configOptions.scope ?? '',
|
||||
user,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setState({
|
||||
...state,
|
||||
user,
|
||||
});
|
||||
}, [user]);
|
||||
|
||||
const setUser = (user?: User) => {
|
||||
setState({
|
||||
...state,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
...state,
|
||||
setUser,
|
||||
}}>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppContextProvider;
|
|
@ -1,9 +1,7 @@
|
|||
import { createBrowserHistory } from 'history';
|
||||
import React, { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React from 'react';
|
||||
import { Route as ReactRouterDomRoute, Switch, Router } from 'react-router-dom';
|
||||
|
||||
import AppContext from './AppContext';
|
||||
import loadable from './utils/loadable';
|
||||
|
||||
const NotFound = loadable(
|
||||
|
@ -28,22 +26,11 @@ export const history = createBrowserHistory({
|
|||
});
|
||||
|
||||
const AppRoute: React.FC = () => {
|
||||
const appContext = useContext(AppContext);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!appContext) {
|
||||
throw Error(t('app-context-not-correct-used'));
|
||||
}
|
||||
|
||||
const { user } = appContext;
|
||||
|
||||
const isUserLoggedIn = user?.username;
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<ReactRouterDomRoute exact={true} path={Route.ROOT}>
|
||||
<HomePage isUserLoggedIn={!!isUserLoggedIn} />
|
||||
<HomePage />
|
||||
</ReactRouterDomRoute>
|
||||
<ReactRouterDomRoute exact={true} path={Route.PACKAGE}>
|
||||
<VersionContextProvider>
|
||||
|
|
|
@ -2,79 +2,78 @@ import React from 'react';
|
|||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
render,
|
||||
renderWithStore,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
cleanup,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
} from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
|
||||
import { AppContextProvider } from '../../App';
|
||||
import translationEN from '../../i18n/crowdin/ui.json';
|
||||
import { store } from '../../store';
|
||||
|
||||
import Header from './Header';
|
||||
|
||||
const props = {
|
||||
user: {
|
||||
username: 'verddacio-user',
|
||||
},
|
||||
packages: [],
|
||||
};
|
||||
|
||||
/* eslint-disable react/jsx-no-bind*/
|
||||
describe('<Header /> component with logged in state', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
test('should load the component in logged out state', () => {
|
||||
render(
|
||||
renderWithStore(
|
||||
<Router>
|
||||
<AppContextProvider>
|
||||
<Header />
|
||||
</AppContextProvider>
|
||||
</Router>
|
||||
<Header />
|
||||
</Router>,
|
||||
store
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('header--menu-accountcircle')).toBeNull();
|
||||
expect(screen.queryByTestId('logInDialogIcon')).toBeNull();
|
||||
expect(screen.getByText('Login')).toBeTruthy();
|
||||
expect(screen.queryByTestId('header--button-login')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should load the component in logged in state', () => {
|
||||
const { getByTestId, queryByText } = render(
|
||||
test('should load the component in logged in state', async () => {
|
||||
renderWithStore(
|
||||
<Router>
|
||||
<AppContextProvider user={props.user}>
|
||||
<Header />
|
||||
</AppContextProvider>
|
||||
</Router>
|
||||
<Header />
|
||||
</Router>,
|
||||
store
|
||||
);
|
||||
store.dispatch.login.logInUser({ username: 'store', token: '12345' });
|
||||
|
||||
expect(getByTestId('header--menu-accountcircle')).toBeTruthy();
|
||||
expect(queryByText('Login')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('logInDialogIcon')).toBeTruthy();
|
||||
expect(screen.queryByText('Login')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('should open login dialog', async () => {
|
||||
const { getByTestId } = render(
|
||||
renderWithStore(
|
||||
<Router>
|
||||
<AppContextProvider>
|
||||
<Header />
|
||||
</AppContextProvider>
|
||||
</Router>
|
||||
<Header />
|
||||
</Router>,
|
||||
store
|
||||
);
|
||||
|
||||
const loginBtn = getByTestId('header--button-login');
|
||||
store.dispatch.login.logOutUser();
|
||||
|
||||
const loginBtn = screen.getByTestId('header--button-login');
|
||||
fireEvent.click(loginBtn);
|
||||
const loginDialog = await waitFor(() => getByTestId('login--dialog'));
|
||||
const loginDialog = await waitFor(() => screen.getByTestId('login--dialog'));
|
||||
expect(loginDialog).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should logout the user', async () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
const { getByText, getByTestId } = renderWithStore(
|
||||
<Router>
|
||||
<AppContextProvider user={props.user}>
|
||||
<Header />
|
||||
</AppContextProvider>
|
||||
</Router>
|
||||
<Header />
|
||||
</Router>,
|
||||
store
|
||||
);
|
||||
|
||||
const headerMenuAccountCircle = getByTestId('header--menu-accountcircle');
|
||||
store.dispatch.login.logInUser({ username: 'store', token: '12345' });
|
||||
|
||||
const headerMenuAccountCircle = getByTestId('logInDialogIcon');
|
||||
fireEvent.click(headerMenuAccountCircle);
|
||||
|
||||
// wait for button Logout's appearance and return the element
|
||||
|
@ -84,12 +83,11 @@ 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(
|
||||
const { getByTestId } = renderWithStore(
|
||||
<Router>
|
||||
<AppContextProvider user={props.user}>
|
||||
<Header />
|
||||
</AppContextProvider>
|
||||
</Router>
|
||||
<Header />
|
||||
</Router>,
|
||||
store
|
||||
);
|
||||
|
||||
const documentationBtn = getByTestId('header--tooltip-documentation');
|
||||
|
@ -99,12 +97,11 @@ describe('<Header /> component with logged in state', () => {
|
|||
});
|
||||
|
||||
test('should open the registrationInfo modal when clicking on the info icon', async () => {
|
||||
const { getByTestId } = render(
|
||||
const { getByTestId } = renderWithStore(
|
||||
<Router>
|
||||
<AppContextProvider user={props.user}>
|
||||
<Header />
|
||||
</AppContextProvider>
|
||||
</Router>
|
||||
<Header />
|
||||
</Router>,
|
||||
store
|
||||
);
|
||||
|
||||
const infoBtn = getByTestId('header--tooltip-info');
|
||||
|
@ -116,12 +113,11 @@ 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(
|
||||
const { getByTestId, getByText, queryByTestId } = renderWithStore(
|
||||
<Router>
|
||||
<AppContextProvider user={props.user}>
|
||||
<Header />
|
||||
</AppContextProvider>
|
||||
</Router>
|
||||
<Header />
|
||||
</Router>,
|
||||
store
|
||||
);
|
||||
|
||||
const infoBtn = getByTestId('header--tooltip-info');
|
||||
|
@ -138,17 +134,15 @@ describe('<Header /> component with logged in state', () => {
|
|||
});
|
||||
|
||||
test('should hide login if is disabled', () => {
|
||||
// @ts-expect-error
|
||||
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
|
||||
base: 'foo',
|
||||
login: false,
|
||||
};
|
||||
render(
|
||||
renderWithStore(
|
||||
<Router>
|
||||
<AppContextProvider user={props.user}>
|
||||
<Header />
|
||||
</AppContextProvider>
|
||||
</Router>
|
||||
<Header />
|
||||
</Router>,
|
||||
store
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('header--button-login')).not.toBeInTheDocument();
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Button from 'verdaccio-ui/components/Button';
|
||||
import { useConfig } from 'verdaccio-ui/providers/config';
|
||||
import storage from 'verdaccio-ui/utils/storage';
|
||||
|
||||
import AppContext from '../../App/AppContext';
|
||||
import { Dispatch, RootState } from '../../store/store';
|
||||
|
||||
import HeaderInfoDialog from './HeaderInfoDialog';
|
||||
import HeaderLeft from './HeaderLeft';
|
||||
|
@ -21,26 +21,15 @@ interface Props {
|
|||
/* eslint-disable react/jsx-no-bind*/
|
||||
const Header: React.FC<Props> = ({ withoutSearch }) => {
|
||||
const { t } = useTranslation();
|
||||
const appContext = useContext(AppContext);
|
||||
const [isInfoDialogOpen, setOpenInfoDialog] = useState<boolean>(false);
|
||||
const [showMobileNavBar, setShowMobileNavBar] = useState<boolean>(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState<boolean>(false);
|
||||
|
||||
if (!appContext) {
|
||||
throw Error(t('app-context-not-correct-used'));
|
||||
}
|
||||
|
||||
const { user, scope, setUser } = appContext;
|
||||
const loginStore = useSelector((state: RootState) => state.login);
|
||||
const configStore = useSelector((state: RootState) => state.configuration);
|
||||
const { configOptions } = useConfig();
|
||||
|
||||
/**
|
||||
* Logouts user
|
||||
* Required by: <Header />
|
||||
*/
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
const handleLogout = () => {
|
||||
storage.removeItem('username');
|
||||
storage.removeItem('token');
|
||||
setUser(undefined);
|
||||
dispatch.login.logOutUser();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -54,7 +43,7 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
|
|||
onOpenRegistryInfoDialog={() => setOpenInfoDialog(true)}
|
||||
onToggleLogin={() => setShowLoginModal(!showLoginModal)}
|
||||
onToggleMobileNav={() => setShowMobileNavBar(!showMobileNavBar)}
|
||||
username={user?.username}
|
||||
username={loginStore?.username}
|
||||
withoutSearch={withoutSearch}
|
||||
/>
|
||||
</InnerNavBar>
|
||||
|
@ -62,7 +51,7 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
|
|||
isOpen={isInfoDialogOpen}
|
||||
onCloseDialog={() => setOpenInfoDialog(false)}
|
||||
registryUrl={configOptions.base}
|
||||
scope={scope}
|
||||
scope={configStore.scope}
|
||||
/>
|
||||
</NavBar>
|
||||
{showMobileNavBar && !withoutSearch && (
|
||||
|
@ -75,7 +64,9 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
|
|||
</Button>
|
||||
</MobileNavBar>
|
||||
)}
|
||||
{!user && <LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />}
|
||||
{!loginStore.user && (
|
||||
<LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@ const HeaderMenu: React.FC<Props> = ({
|
|||
<>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
data-testid="header--menu-accountcircle"
|
||||
data-testid="logInDialogIcon"
|
||||
id="header--button-account"
|
||||
onClick={onLoggedInMenu}>
|
||||
<AccountCircle />
|
||||
|
@ -52,8 +52,8 @@ const HeaderMenu: React.FC<Props> = ({
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
button={true}
|
||||
data-testid="header--button-logout"
|
||||
id="header--button-logout"
|
||||
data-testid="logOutDialogIcon"
|
||||
id="logOutDialogIcon"
|
||||
onClick={onLogout}>
|
||||
{t('button.logout')}
|
||||
</MenuItem>
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
|
||||
import api from 'verdaccio-ui/providers/API/api';
|
||||
import {
|
||||
render,
|
||||
renderWithStore,
|
||||
waitFor,
|
||||
fireEvent,
|
||||
cleanup,
|
||||
|
@ -10,15 +10,10 @@ import {
|
|||
act,
|
||||
} from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
|
||||
import AppContext, { AppContextProps } from '../../AppContext';
|
||||
import { store } from '../../../store';
|
||||
|
||||
import LoginDialog from './LoginDialog';
|
||||
|
||||
const appContextValue: AppContextProps = {
|
||||
scope: '',
|
||||
setUser: jest.fn(),
|
||||
};
|
||||
|
||||
describe('<LoginDialog /> component', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
@ -30,11 +25,7 @@ describe('<LoginDialog /> component', () => {
|
|||
const props = {
|
||||
onClose: jest.fn(),
|
||||
};
|
||||
const { container } = render(
|
||||
<AppContext.Provider value={appContextValue}>
|
||||
<LoginDialog onClose={props.onClose} />
|
||||
</AppContext.Provider>
|
||||
);
|
||||
const { container } = renderWithStore(<LoginDialog onClose={props.onClose} />, store);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -44,10 +35,9 @@ describe('<LoginDialog /> component', () => {
|
|||
onClose: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<AppContext.Provider value={appContextValue}>
|
||||
<LoginDialog onClose={props.onClose} open={props.open} />
|
||||
</AppContext.Provider>
|
||||
const { getByTestId } = renderWithStore(
|
||||
<LoginDialog onClose={props.onClose} open={props.open} />,
|
||||
store
|
||||
);
|
||||
|
||||
const loginDialogHeading = await waitFor(() => getByTestId('login-dialog-form-login-button'));
|
||||
|
@ -60,18 +50,18 @@ describe('<LoginDialog /> component', () => {
|
|||
onClose: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<AppContext.Provider value={appContextValue}>
|
||||
<LoginDialog onClose={props.onClose} open={props.open} />
|
||||
</AppContext.Provider>
|
||||
const { getByTestId } = renderWithStore(
|
||||
<LoginDialog onClose={props.onClose} open={props.open} />,
|
||||
store
|
||||
);
|
||||
|
||||
const loginDialogButton = await waitFor(() => getByTestId('close-login-dialog-button'));
|
||||
expect(loginDialogButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
await act(() => {
|
||||
fireEvent.click(loginDialogButton, { open: false });
|
||||
});
|
||||
|
||||
expect(props.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -88,11 +78,9 @@ describe('<LoginDialog /> component', () => {
|
|||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<AppContext.Provider value={appContextValue}>
|
||||
<LoginDialog onClose={props.onClose} open={props.open} />
|
||||
</AppContext.Provider>
|
||||
);
|
||||
await act(async () => {
|
||||
renderWithStore(<LoginDialog onClose={props.onClose} open={props.open} />, store);
|
||||
});
|
||||
|
||||
const userNameInput = screen.getByPlaceholderText('Your username');
|
||||
expect(userNameInput).toBeInTheDocument();
|
||||
|
@ -104,13 +92,18 @@ describe('<LoginDialog /> component', () => {
|
|||
const passwordInput = screen.getByPlaceholderText('Your strong password');
|
||||
expect(userNameInput).toBeInTheDocument();
|
||||
fireEvent.focus(passwordInput);
|
||||
fireEvent.change(passwordInput, { target: { value: '1234' } });
|
||||
|
||||
act(async () => {
|
||||
const signInButton = await screen.getByTestId('login-dialog-form-login-button');
|
||||
expect(signInButton).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
fireEvent.change(passwordInput, { target: { value: '1234' } });
|
||||
});
|
||||
const signInButton = screen.getByTestId('login-dialog-form-login-button');
|
||||
expect(signInButton).not.toBeDisabled();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(signInButton);
|
||||
});
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
// screen.debug();
|
||||
});
|
||||
|
||||
test.todo('validateCredentials: should validate credentials');
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import i18next from 'i18next';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import React, { useState, useContext, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Dialog from 'verdaccio-ui/components/Dialog';
|
||||
import DialogContent from 'verdaccio-ui/components/DialogContent';
|
||||
import { useAPI, LoginBody } from 'verdaccio-ui/providers/API/APIProvider';
|
||||
import { LoginError } from 'verdaccio-ui/utils/login';
|
||||
import storage from 'verdaccio-ui/utils/storage';
|
||||
|
||||
import AppContext from '../../../App/AppContext';
|
||||
import { LoginBody } from '../../../store/models/login';
|
||||
import { Dispatch, RootState } from '../../../store/store';
|
||||
|
||||
import LoginDialogCloseButton from './LoginDialogCloseButton';
|
||||
import LoginDialogForm, { FormValues } from './LoginDialogForm';
|
||||
|
@ -21,62 +19,48 @@ interface Props {
|
|||
}
|
||||
|
||||
const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const appContext = useContext(AppContext);
|
||||
const { doLogin } = useAPI();
|
||||
|
||||
const loginStore = useSelector((state: RootState) => state.login);
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
const makeLogin = useCallback(
|
||||
async (username?: string, password?: string): Promise<LoginBody> => {
|
||||
async (username?: string, password?: string): Promise<LoginBody | void> => {
|
||||
// checks isEmpty
|
||||
if (isEmpty(username) || isEmpty(password)) {
|
||||
const error = {
|
||||
dispatch.login.addError({
|
||||
type: 'error',
|
||||
description: i18next.t('form-validation.username-or-password-cant-be-empty'),
|
||||
};
|
||||
return { error };
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response: LoginBody = await doLogin(username as string, password as string);
|
||||
|
||||
return response;
|
||||
dispatch.login.getUser({ username, password });
|
||||
// const response: LoginBody = await doLogin(username as string, password as string);
|
||||
dispatch.login.clearError();
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('login error', e.message);
|
||||
const error = {
|
||||
dispatch.login.addError({
|
||||
type: 'error',
|
||||
description: i18next.t('form-validation.unable-to-sign-in'),
|
||||
};
|
||||
return { error };
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('login error', e.message);
|
||||
}
|
||||
},
|
||||
[doLogin]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
if (!appContext) {
|
||||
throw Error(t('app-context-not-correct-used'));
|
||||
}
|
||||
|
||||
const [error, setError] = useState<LoginError>();
|
||||
|
||||
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();
|
||||
}
|
||||
await makeLogin(data.username, data.password);
|
||||
},
|
||||
[appContext, onClose, makeLogin]
|
||||
[makeLogin]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStore.token && typeof loginStore.error === 'undefined') {
|
||||
onClose();
|
||||
}
|
||||
}, [loginStore, onClose]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
data-testid="login--dialog"
|
||||
|
@ -88,7 +72,7 @@ const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
|
|||
<LoginDialogCloseButton onClose={onClose} />
|
||||
<DialogContent>
|
||||
<LoginDialogHeader />
|
||||
<LoginDialogForm error={error} onSubmit={handleDoLogin} />
|
||||
<LoginDialogForm error={loginStore.error} onSubmit={handleDoLogin} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,6 @@ describe('<RegistryInfoContent /> component', () => {
|
|||
const props = { registryUrl: 'http://localhost:4872', scope: '@' };
|
||||
render(<RegistryInfoContent registryUrl={props.registryUrl} scope={props.scope} />);
|
||||
|
||||
screen.debug();
|
||||
expect(screen.getByText('pnpm set @:registry http://localhost:4872')).toBeInTheDocument();
|
||||
expect(screen.getByText('pnpm adduser --registry http://localhost:4872')).toBeInTheDocument();
|
||||
expect(
|
||||
|
|
|
@ -2,7 +2,9 @@ import React from 'react';
|
|||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import api from 'verdaccio-ui/providers/API/api';
|
||||
import { render, fireEvent, waitFor } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
import { renderWithStore, fireEvent, waitFor } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
|
||||
import { store } from '../../../store/store';
|
||||
|
||||
jest.mock('lodash/debounce', () =>
|
||||
jest.fn((fn) => {
|
||||
|
@ -41,12 +43,15 @@ describe('<Search /> component', () => {
|
|||
});
|
||||
|
||||
test('should load the component in default state', () => {
|
||||
const { container } = render(<ComponentToBeRendered />);
|
||||
const { container } = renderWithStore(<ComponentToBeRendered />, store);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handleSearch: when user type package name in search component, show suggestions', async () => {
|
||||
const { getByPlaceholderText, getAllByText } = render(<ComponentToBeRendered />);
|
||||
const { getByPlaceholderText, getAllByText } = renderWithStore(
|
||||
<ComponentToBeRendered />,
|
||||
store
|
||||
);
|
||||
|
||||
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||
|
||||
|
@ -62,7 +67,10 @@ describe('<Search /> component', () => {
|
|||
});
|
||||
|
||||
test('onBlur: should cancel all search requests', async () => {
|
||||
const { getByPlaceholderText, getByRole, getAllByText } = render(<ComponentToBeRendered />);
|
||||
const { getByPlaceholderText, getByRole, getAllByText } = renderWithStore(
|
||||
<ComponentToBeRendered />,
|
||||
store
|
||||
);
|
||||
|
||||
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||
|
||||
|
@ -80,7 +88,7 @@ describe('<Search /> component', () => {
|
|||
});
|
||||
|
||||
test('handleSearch: cancel all search requests when there is no value in search component with type method', async () => {
|
||||
const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
|
||||
const { getByPlaceholderText, getByRole } = renderWithStore(<ComponentToBeRendered />, store);
|
||||
|
||||
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||
fireEvent.focus(autoCompleteInput);
|
||||
|
@ -92,7 +100,7 @@ describe('<Search /> component', () => {
|
|||
});
|
||||
|
||||
test('handleSearch: when method is not type method', async () => {
|
||||
const { getByPlaceholderText, getByRole } = render(<ComponentToBeRendered />);
|
||||
const { getByPlaceholderText, getByRole } = renderWithStore(<ComponentToBeRendered />, store);
|
||||
|
||||
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||
|
||||
|
@ -105,7 +113,7 @@ describe('<Search /> component', () => {
|
|||
});
|
||||
|
||||
test('handleSearch: loading is been displayed', async () => {
|
||||
const { getByPlaceholderText, getByText } = render(<ComponentToBeRendered />);
|
||||
const { getByPlaceholderText, getByText } = renderWithStore(<ComponentToBeRendered />, store);
|
||||
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||
|
||||
fireEvent.focus(autoCompleteInput);
|
||||
|
@ -117,7 +125,10 @@ describe('<Search /> component', () => {
|
|||
});
|
||||
|
||||
test('handlePackagesClearRequested: should clear suggestions', async () => {
|
||||
const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
|
||||
const { getByPlaceholderText, getAllByText, getByRole } = renderWithStore(
|
||||
<ComponentToBeRendered />,
|
||||
store
|
||||
);
|
||||
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||
|
||||
fireEvent.focus(autoCompleteInput);
|
||||
|
@ -135,7 +146,10 @@ describe('<Search /> component', () => {
|
|||
});
|
||||
|
||||
test('handleClickSearch: should change the window location on click or return key', async () => {
|
||||
const { getByPlaceholderText, getAllByText, getByRole } = render(<ComponentToBeRendered />);
|
||||
const { getByPlaceholderText, getAllByText, getByRole } = renderWithStore(
|
||||
<ComponentToBeRendered />,
|
||||
store
|
||||
);
|
||||
const autoCompleteInput = getByPlaceholderText('Search Packages');
|
||||
|
||||
fireEvent.focus(autoCompleteInput);
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React, { useState, FormEvent, useCallback, useRef, useEffect } from 'react';
|
||||
import React, { useState, FormEvent, useCallback } from 'react';
|
||||
import { SuggestionSelectedEventData } from 'react-autosuggest';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
|
||||
import AutoComplete from 'verdaccio-ui/components/AutoComplete';
|
||||
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
|
||||
|
||||
import { Dispatch, RootState } from '../../../store/store';
|
||||
|
||||
import SearchAdornment from './SearchAdornment';
|
||||
|
||||
|
@ -16,22 +18,18 @@ const CONSTANTS = {
|
|||
|
||||
const Search: React.FC<RouteComponentProps> = ({ history }) => {
|
||||
const { t } = useTranslation();
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const mountedRef = useRef(true);
|
||||
const [requestList, setRequestList] = useState<{ abort: () => void }[]>([]);
|
||||
const { callSearch } = useAPI();
|
||||
|
||||
// const mountedRef = useRef(true);
|
||||
const { isError, suggestions } = useSelector((state: RootState) => state.search);
|
||||
const isLoading = useSelector((state: RootState) => state?.loading?.models.search);
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
/**
|
||||
* Cancel all the requests which are in pending state.
|
||||
*/
|
||||
const cancelAllSearchRequests = useCallback(() => {
|
||||
requestList.forEach((request) => request.abort());
|
||||
setRequestList([]);
|
||||
}, [requestList, setRequestList]);
|
||||
dispatch.search.clearRequestQueue();
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* As user focuses out from input, we cancel all the request from requestList
|
||||
|
@ -41,12 +39,9 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
|
|||
(event: FormEvent<HTMLInputElement>) => {
|
||||
// stops event bubbling
|
||||
event.stopPropagation();
|
||||
setLoaded(false);
|
||||
setLoading(false);
|
||||
setError(false);
|
||||
cancelAllSearchRequests();
|
||||
},
|
||||
[setLoaded, setLoading, cancelAllSearchRequests, setError]
|
||||
[cancelAllSearchRequests]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -58,9 +53,6 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
|
|||
event.stopPropagation();
|
||||
if (method === 'type') {
|
||||
const value = newValue.trim();
|
||||
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
setSearch(value);
|
||||
setLoaded(false);
|
||||
/**
|
||||
|
@ -79,8 +71,8 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
|
|||
* Cancel all the request from list and make request list empty.
|
||||
*/
|
||||
const handlePackagesClearRequested = useCallback(() => {
|
||||
setSuggestions([]);
|
||||
}, [setSuggestions]);
|
||||
dispatch.search.saveSearch({ suggestions: [] });
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* When an user select any package by clicking or pressing return key.
|
||||
|
@ -108,46 +100,12 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
|
|||
* 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;
|
||||
if (!mountedRef.current) {
|
||||
return null;
|
||||
}
|
||||
// Keep track of search requests.
|
||||
setRequestList([...requestList, controller]);
|
||||
const suggestions = await callSearch(value, signal);
|
||||
// FIXME: Argument of type 'unknown' is not assignable to parameter of type 'SetStateAction<never[]>'
|
||||
setSuggestions(suggestions as any);
|
||||
setLoaded(true);
|
||||
} catch (error: any) {
|
||||
/**
|
||||
* 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 {
|
||||
if (mountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
({ value }: { value: string }) => {
|
||||
dispatch.search.getSuggestions({ value });
|
||||
},
|
||||
[requestList, setRequestList, setSuggestions, setLoaded, setError, setLoading, callSearch]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
onBlur={handleOnBlur}
|
||||
|
@ -158,9 +116,9 @@ const Search: React.FC<RouteComponentProps> = ({ history }) => {
|
|||
placeholder={t('search.packages')}
|
||||
startAdornment={<SearchAdornment />}
|
||||
suggestions={suggestions}
|
||||
suggestionsError={error}
|
||||
suggestionsError={isError}
|
||||
suggestionsLoaded={loaded}
|
||||
suggestionsLoading={loading}
|
||||
suggestionsLoading={isLoading}
|
||||
value={search}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export { default } from './App';
|
||||
export { default as AppContextProvider } from './AppContextProvider';
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
import { renderWithStore, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
|
||||
import { DetailContext, DetailContextProps } from '../../pages/Version';
|
||||
import { store } from '../../store/store';
|
||||
|
||||
import ActionBar from './ActionBar';
|
||||
|
||||
|
@ -44,7 +45,10 @@ describe('<ActionBar /> component', () => {
|
|||
});
|
||||
|
||||
test('should render the component in default state', () => {
|
||||
const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
|
||||
const { container } = renderWithStore(
|
||||
<ComponentToBeRendered contextValue={detailContextValue} />,
|
||||
store
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -62,22 +66,25 @@ describe('<ActionBar /> component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />
|
||||
const { container } = renderWithStore(
|
||||
<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />,
|
||||
store
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('when there is a button to download a tarball', () => {
|
||||
const { getByTitle } = render(
|
||||
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
|
||||
const { getByTitle } = renderWithStore(
|
||||
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />,
|
||||
store
|
||||
);
|
||||
expect(getByTitle('Download tarball')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('when there is a button to open an issue', () => {
|
||||
const { getByTitle } = render(
|
||||
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
|
||||
const { getByTitle } = renderWithStore(
|
||||
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />,
|
||||
store
|
||||
);
|
||||
expect(getByTitle('Open an issue')).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -4,11 +4,11 @@ import DownloadIcon from '@material-ui/icons/CloudDownload';
|
|||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Theme } from 'verdaccio-ui/design-tokens/theme';
|
||||
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
|
||||
import { extractFileName, downloadFile } from 'verdaccio-ui/utils/url';
|
||||
|
||||
import { Dispatch } from '../../store/store';
|
||||
import FloatingActionButton from '../FloatingActionButton';
|
||||
import Link from '../Link';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
@ -34,13 +34,11 @@ export interface ActionBarActionProps {
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
|
||||
const { t } = useTranslation();
|
||||
const { getResource } = useAPI();
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
const fileStream = await getResource(link);
|
||||
const fileName = extractFileName(link);
|
||||
downloadFile(fileStream, fileName);
|
||||
}, [getResource, link]);
|
||||
dispatch.download.getTarball({ link });
|
||||
}, [dispatch, link]);
|
||||
|
||||
switch (type) {
|
||||
case 'VISIT_HOMEPAGE':
|
||||
|
|
|
@ -51,7 +51,6 @@ describe('<Author /> component', () => {
|
|||
};
|
||||
|
||||
const wrapper = render(withAuthorComponent(packageMeta));
|
||||
wrapper.debug();
|
||||
expect(wrapper.queryAllByText('verdaccio')).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ function loadTranslationFile(lng) {
|
|||
return require(`./download_translations/${lng}/ui.json`);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`language ${lng} file not found, fallback to en-US`);
|
||||
console.warn(`language ${lng} file not found, fallback to en-US`);
|
||||
// in case the file is not there, fallback to en-US
|
||||
return require(`./crowdin/ui.json`);
|
||||
}
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { AppContainer } from 'react-hot-loader';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import APIProvider from 'verdaccio-ui/providers/API/APIProvider';
|
||||
import AppConfigurationContext from 'verdaccio-ui/providers/config';
|
||||
|
||||
import App from './App';
|
||||
import StyleBaseline from './design-tokens/StyleBaseline';
|
||||
import ThemeProvider from './design-tokens/ThemeProvider';
|
||||
import { store } from './store';
|
||||
|
||||
const rootNode = document.getElementById('root');
|
||||
const renderApp = (Component: React.ElementType): void => {
|
||||
ReactDOM.render(
|
||||
<AppContainer>
|
||||
<AppConfigurationContext>
|
||||
<ThemeProvider>
|
||||
<StyleBaseline />
|
||||
<APIProvider>
|
||||
<Provider store={store}>
|
||||
<AppContainer>
|
||||
<AppConfigurationContext>
|
||||
<ThemeProvider>
|
||||
<StyleBaseline />
|
||||
<Component />
|
||||
</APIProvider>
|
||||
</ThemeProvider>
|
||||
</AppConfigurationContext>
|
||||
</AppContainer>,
|
||||
</ThemeProvider>
|
||||
</AppConfigurationContext>
|
||||
</AppContainer>
|
||||
</Provider>,
|
||||
rootNode
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,12 +9,6 @@ import UpLinks from './UpLinks';
|
|||
describe('<UpLinks /> component', () => {
|
||||
beforeEach(cleanup);
|
||||
|
||||
test('should return null without packageMeta', () => {
|
||||
const wrapper = render(<UpLinks />);
|
||||
wrapper.debug();
|
||||
// expect(wrapper).toBeNull();
|
||||
});
|
||||
|
||||
test('should render the component when there is no uplink', () => {
|
||||
const packageMeta = {
|
||||
latest: {
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import ActionBar from 'verdaccio-ui/components/ActionBar';
|
||||
import { render, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
|
||||
import { DetailContext } from '../../context';
|
||||
import { DetailContextProps } from '../../version-config';
|
||||
|
||||
const detailContextValue: DetailContextProps = {
|
||||
packageName: 'foo',
|
||||
readMe: 'test',
|
||||
enableLoading: () => {},
|
||||
isLoading: false,
|
||||
hasNotBeenFound: false,
|
||||
packageMeta: {
|
||||
_uplinks: {},
|
||||
latest: {
|
||||
name: 'verdaccio-ui/local-storage',
|
||||
version: '8.0.1-next.1',
|
||||
dist: {
|
||||
fileCount: 0,
|
||||
unpackedSize: 0,
|
||||
tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
|
||||
},
|
||||
homepage: 'https://verdaccio.org',
|
||||
bugs: {
|
||||
url: 'https://github.com/verdaccio/monorepo/issues',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({
|
||||
contextValue,
|
||||
}) => (
|
||||
<DetailContext.Provider value={contextValue}>
|
||||
<ActionBar />
|
||||
</DetailContext.Provider>
|
||||
);
|
||||
|
||||
describe('<ActionBar /> component', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('should render the component in default state', () => {
|
||||
const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('when there is no action bar data', () => {
|
||||
const packageMeta = {
|
||||
...detailContextValue.packageMeta,
|
||||
latest: {
|
||||
...detailContextValue.packageMeta.latest,
|
||||
homepage: undefined,
|
||||
bugs: undefined,
|
||||
dist: {
|
||||
...detailContextValue.packageMeta.latest.dist,
|
||||
tarball: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('when there is a button to download a tarball', () => {
|
||||
const { getByTitle } = render(
|
||||
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
|
||||
);
|
||||
expect(getByTitle('Download tarball')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('when there is a button to open an issue', () => {
|
||||
const { getByTitle } = render(
|
||||
<ComponentToBeRendered contextValue={{ ...detailContextValue }} />
|
||||
);
|
||||
expect(getByTitle('Open an issue')).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,122 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ActionBar /> component should render the component in default state 1`] = `
|
||||
.emotion-0 {
|
||||
background-color: #4b5e40;
|
||||
color: #fff;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.emotion-0:hover {
|
||||
color: #4b5e40;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
<div
|
||||
class="MuiBox-root MuiBox-root-1"
|
||||
>
|
||||
<a
|
||||
class=""
|
||||
href="https://verdaccio.org"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title="Visit homepage"
|
||||
>
|
||||
<span
|
||||
class="MuiTypography-root MuiTypography-subtitle1"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
|
||||
data-testid="fab"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiFab-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
class=""
|
||||
href="https://github.com/verdaccio/monorepo/issues"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title="Open an issue"
|
||||
>
|
||||
<span
|
||||
class="MuiTypography-root MuiTypography-subtitle1"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
|
||||
data-testid="fab"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiFab-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
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>
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiFab-root emotion-0 emotion-1 MuiFab-sizeSmall"
|
||||
data-testid="fab"
|
||||
tabindex="0"
|
||||
title="Download tarball"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiFab-label"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
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 no action bar data 1`] = `
|
||||
<div
|
||||
class="MuiBox-root MuiBox-root-2"
|
||||
/>
|
||||
`;
|
|
@ -1,20 +1,37 @@
|
|||
import React, { useContext } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import Loading from 'verdaccio-ui/components/Loading';
|
||||
import NotFound from 'verdaccio-ui/components/NotFound';
|
||||
|
||||
import { DetailContext } from './context';
|
||||
import { Dispatch, RootState } from '../../store/store';
|
||||
|
||||
import getRouterPackageName from './get-route-package-name';
|
||||
import VersionLayout from './VersionLayout';
|
||||
|
||||
interface Params {
|
||||
scope?: string;
|
||||
package: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
const Version: React.FC = () => {
|
||||
const detailContext = useContext(DetailContext);
|
||||
const { isLoading, hasNotBeenFound } = detailContext;
|
||||
const { version: packageVersion, package: pkgName, scope } = useParams<Params>();
|
||||
const manifestStore = useSelector((state: RootState) => state.manifest);
|
||||
const isLoading = useSelector((state: RootState) => state?.loading?.models.manifest);
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
|
||||
useEffect(() => {
|
||||
const packageName = getRouterPackageName(pkgName, scope);
|
||||
dispatch.manifest.getManifest({ packageName, packageVersion });
|
||||
}, [dispatch, pkgName, scope, packageVersion]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (hasNotBeenFound) {
|
||||
if (manifestStore.hasNotBeenFound) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { PackageMetaInterface } from 'types/packageMeta';
|
||||
|
||||
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
|
||||
import { Dispatch, RootState } from '../../store/store';
|
||||
|
||||
import { DetailContext } from './context';
|
||||
import getRouterPackageName from './get-route-package-name';
|
||||
import isPackageVersionValid from './is-package-version-valid';
|
||||
|
||||
interface Params {
|
||||
scope?: string;
|
||||
|
@ -15,52 +14,23 @@ interface Params {
|
|||
}
|
||||
|
||||
const VersionContextProvider: React.FC = ({ children }) => {
|
||||
const { version, package: pkgName, scope } = useParams<Params>();
|
||||
const [packageName, setPackageName] = useState(getRouterPackageName(pkgName, scope));
|
||||
const [packageVersion, setPackageVersion] = useState(version);
|
||||
const [packageMeta, setPackageMeta] = useState<PackageMetaInterface>();
|
||||
const [readMe, setReadme] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [hasNotBeenFound, setHasNotBeenFound] = useState<boolean>();
|
||||
const { callDetailPage, callReadme } = useAPI();
|
||||
|
||||
const { version: packageVersion, package: pkgName, scope } = useParams<Params>();
|
||||
const { manifest, readme, packageName, hasNotBeenFound } = useSelector(
|
||||
(state: RootState) => state.manifest
|
||||
);
|
||||
const isLoading = useSelector((state: RootState) => state?.loading?.models.manifest);
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
useEffect(() => {
|
||||
const updatedPackageName = getRouterPackageName(pkgName, scope);
|
||||
setPackageName(updatedPackageName);
|
||||
}, [pkgName, scope]);
|
||||
|
||||
useEffect(() => {
|
||||
setPackageVersion(version);
|
||||
}, [version]);
|
||||
|
||||
const doCalls = useCallback(async () => {
|
||||
try {
|
||||
const packageMeta = await callDetailPage(packageName, packageVersion);
|
||||
const readMe = await callReadme(packageName, packageVersion);
|
||||
if (isPackageVersionValid(packageMeta, packageVersion)) {
|
||||
setReadme(readMe);
|
||||
setPackageMeta(packageMeta);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setHasNotBeenFound(true);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setHasNotBeenFound(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [packageName, packageVersion, callDetailPage, callReadme]);
|
||||
|
||||
useEffect(() => {
|
||||
doCalls();
|
||||
}, [doCalls]);
|
||||
const packageName = getRouterPackageName(pkgName, scope);
|
||||
dispatch.manifest.getManifest({ packageName, packageVersion });
|
||||
}, [dispatch, packageVersion, pkgName, scope]);
|
||||
|
||||
return (
|
||||
<DetailContext.Provider
|
||||
value={{
|
||||
packageMeta,
|
||||
packageMeta: manifest,
|
||||
packageVersion,
|
||||
readMe,
|
||||
readMe: readme,
|
||||
packageName,
|
||||
isLoading,
|
||||
hasNotBeenFound,
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import { PackageMetaInterface } from '../../../types/packageMeta';
|
||||
|
||||
function isPackageVersionValid(
|
||||
packageMeta: Partial<PackageMetaInterface>,
|
||||
packageVersion?: string
|
||||
): boolean {
|
||||
if (!packageVersion || typeof packageVersion === 'undefined') {
|
||||
// if is undefined, that means versions does not exist, we continue
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packageMeta.versions) {
|
||||
return Object.keys(packageMeta.versions).includes(packageVersion);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default isPackageVersionValid;
|
|
@ -1,36 +1,19 @@
|
|||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Loading from 'verdaccio-ui/components/Loading';
|
||||
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
|
||||
|
||||
import { Dispatch, RootState } from '../../store/store';
|
||||
|
||||
import { PackageList } from './PackageList';
|
||||
|
||||
interface Props {
|
||||
isUserLoggedIn: boolean;
|
||||
}
|
||||
|
||||
const Home: React.FC<Props> = ({ isUserLoggedIn }) => {
|
||||
const [packages, setPackages] = useState([]);
|
||||
const { getPackages } = useAPI();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const loadPackages = useCallback(async () => {
|
||||
try {
|
||||
const packages = await getPackages();
|
||||
// FIXME add correct type for package
|
||||
setPackages(packages as never[]);
|
||||
} catch (error: any) {
|
||||
// FIXME: add dialog
|
||||
// eslint-disable-next-line no-console
|
||||
console.error({
|
||||
title: 'Warning',
|
||||
message: `Unable to load package list: ${error.message}`,
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [getPackages]);
|
||||
const Home: React.FC = () => {
|
||||
const packages = useSelector((state: RootState) => state.packages.response);
|
||||
const isLoading = useSelector((state: RootState) => state?.loading?.models.packages);
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
useEffect(() => {
|
||||
loadPackages().then();
|
||||
}, [isUserLoggedIn, loadPackages]);
|
||||
dispatch.packages.getPackages();
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className="container content" data-testid="home-page-container">
|
||||
|
|
|
@ -63,7 +63,7 @@ exports[`<Help /> component should load the component in default state 1`] = `
|
|||
<span
|
||||
class="emotion-6 emotion-7"
|
||||
>
|
||||
npm adduser --registry http://localhost
|
||||
npm adduser --registry http://localhost:9000/
|
||||
</span>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiIconButton-root"
|
||||
|
@ -102,7 +102,7 @@ exports[`<Help /> component should load the component in default state 1`] = `
|
|||
<span
|
||||
class="emotion-6 emotion-7"
|
||||
>
|
||||
npm publish --registry http://localhost
|
||||
npm publish --registry http://localhost:9000/
|
||||
</span>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiIconButton-root"
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
|
||||
import { render, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
import { renderWithStore, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
|
||||
import { store } from '../../../../store';
|
||||
|
||||
import Package from './Package';
|
||||
|
||||
|
@ -29,7 +31,7 @@ describe('<Package /> component', () => {
|
|||
keywords: ['verdaccio'],
|
||||
};
|
||||
|
||||
const wrapper = render(
|
||||
const wrapper = renderWithStore(
|
||||
<MemoryRouter>
|
||||
<Package
|
||||
author={props.author}
|
||||
|
@ -39,7 +41,8 @@ describe('<Package /> component', () => {
|
|||
time={props.time}
|
||||
version={props.version}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</MemoryRouter>,
|
||||
store
|
||||
);
|
||||
|
||||
// FUTURE: improve this expectectations
|
||||
|
|
|
@ -5,6 +5,7 @@ import DownloadIcon from '@material-ui/icons/CloudDownload';
|
|||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Grid from 'verdaccio-ui/components/Grid';
|
||||
import { Version, FileBinary, Time, Law } from 'verdaccio-ui/components/Icons';
|
||||
|
@ -12,14 +13,12 @@ import Link from 'verdaccio-ui/components/Link';
|
|||
import ListItem from 'verdaccio-ui/components/ListItem';
|
||||
import Tooltip from 'verdaccio-ui/components/Tooltip';
|
||||
import { Theme } from 'verdaccio-ui/design-tokens/theme';
|
||||
import { useAPI } from 'verdaccio-ui/providers/API/APIProvider';
|
||||
import { useConfig } from 'verdaccio-ui/providers/config';
|
||||
import fileSizeSI from 'verdaccio-ui/utils/file-size';
|
||||
import { formatDate, formatDateDistance, getAuthorName } from 'verdaccio-ui/utils/package';
|
||||
import { extractFileName, downloadFile } from 'verdaccio-ui/utils/url';
|
||||
import { isURL } from 'verdaccio-ui/utils/url';
|
||||
|
||||
import { PackageMetaInterface, Author as PackageAuthor } from '../../../../../types/packageMeta';
|
||||
import { Dispatch, RootState } from '../../../../store/store';
|
||||
|
||||
import {
|
||||
Author,
|
||||
|
@ -72,19 +71,17 @@ const Package: React.FC<PackageInterface> = ({
|
|||
time,
|
||||
version,
|
||||
}) => {
|
||||
const config = useSelector((state: RootState) => state.configuration);
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
const { t } = useTranslation();
|
||||
const { getResource } = useAPI();
|
||||
const { configOptions } = useConfig();
|
||||
|
||||
const handleDownload = useCallback(
|
||||
async (tarballDist: string) => {
|
||||
// FIXME: check, the dist might be different thant npmjs
|
||||
const link = tarballDist.replace(`https://registry.npmjs.org/`, configOptions.base as string);
|
||||
const fileStream = await getResource(link);
|
||||
const fileName = extractFileName(link);
|
||||
downloadFile(fileStream, fileName);
|
||||
const link = tarballDist.replace(`https://registry.npmjs.org/`, config.base);
|
||||
dispatch.download.getTarball({ link });
|
||||
},
|
||||
[getResource, configOptions]
|
||||
[dispatch, config]
|
||||
);
|
||||
|
||||
const renderVersionInfo = (): React.ReactNode =>
|
||||
|
|
|
@ -26,7 +26,6 @@ const PackageList: React.FC<Props> = ({ packages }) => {
|
|||
packages[index];
|
||||
// TODO: move format license to API side.
|
||||
const formattedLicense = formatLicense(license);
|
||||
|
||||
return (
|
||||
<CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
|
||||
<div style={style}>
|
||||
|
@ -56,21 +55,23 @@ const PackageList: React.FC<Props> = ({ packages }) => {
|
|||
<WindowScroller>
|
||||
{({ height, isScrolling, scrollTop, onChildScroll }) => (
|
||||
<AutoSizer disableHeight={true}>
|
||||
{({ width }) => (
|
||||
<List
|
||||
autoHeight={true}
|
||||
deferredMeasurementCache={cache}
|
||||
height={height}
|
||||
isScrolling={isScrolling}
|
||||
onScroll={onChildScroll}
|
||||
overscanRowCount={3}
|
||||
rowCount={packages.length}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={renderRow}
|
||||
scrollTop={scrollTop}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
{({ width }) => {
|
||||
return (
|
||||
<List
|
||||
autoHeight={true}
|
||||
deferredMeasurementCache={cache}
|
||||
height={height}
|
||||
isScrolling={isScrolling}
|
||||
onScroll={onChildScroll}
|
||||
overscanRowCount={3}
|
||||
rowCount={packages.length}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={renderRow}
|
||||
scrollTop={scrollTop}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
)}
|
||||
</WindowScroller>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { render, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
import { renderWithStore, cleanup } from 'verdaccio-ui/utils/test-react-testing-library';
|
||||
|
||||
import { store } from '../../../store';
|
||||
|
||||
import { PackageList } from './PackageList';
|
||||
|
||||
|
@ -12,7 +14,7 @@ describe('<PackageList /> component', () => {
|
|||
const props = {
|
||||
packages: [],
|
||||
};
|
||||
const wrapper = render(<PackageList packages={props.packages} />);
|
||||
const wrapper = renderWithStore(<PackageList packages={props.packages} />, store);
|
||||
expect(wrapper.getByText('No Package Published Yet.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
@ -46,10 +48,11 @@ describe('<PackageList /> component', () => {
|
|||
help: false,
|
||||
};
|
||||
|
||||
const wrapper = render(
|
||||
const wrapper = renderWithStore(
|
||||
<BrowserRouter>
|
||||
<PackageList packages={props.packages} />
|
||||
</BrowserRouter>
|
||||
</BrowserRouter>,
|
||||
store
|
||||
);
|
||||
|
||||
expect(wrapper.queryAllByTestId('package-item-list')).toHaveLength(3);
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { Package } from '@verdaccio/types';
|
||||
import React, { createContext, FunctionComponent, useContext, useMemo } from 'react';
|
||||
|
||||
import { useConfig } from 'verdaccio-ui/providers/config';
|
||||
|
||||
import { HEADERS } from '../../../lib/constants';
|
||||
import { PackageMetaInterface } from '../../../types/packageMeta';
|
||||
|
||||
import API from './api';
|
||||
|
||||
type ConfigProviderProps = {
|
||||
callReadme: (packageName: string, packageVersion?: string) => Promise<string>;
|
||||
callDetailPage: (packageName: string, packageVersion?: string) => Promise<PackageMetaInterface>;
|
||||
callSearch: (value: string, signal: AbortSignal) => Promise<string>;
|
||||
getPackages: () => Promise<Package[]>;
|
||||
doLogin: (username: string, password: string) => Promise<LoginBody>;
|
||||
getResource: (link: string) => Promise<Blob>;
|
||||
};
|
||||
|
||||
export interface LoginError {
|
||||
type: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface LoginBody {
|
||||
username?: string;
|
||||
token?: string;
|
||||
error?: LoginError;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const AppAPIContext = createContext<ConfigProviderProps>({});
|
||||
|
||||
const APIProvider: FunctionComponent = ({ children }) => {
|
||||
const { configOptions } = useConfig();
|
||||
|
||||
const buildURL = (basePath: string) => {
|
||||
return `${configOptions?.base}-/verdaccio/${basePath}`;
|
||||
};
|
||||
|
||||
const callReadme = async (packageName: string, packageVersion?: string): Promise<string> => {
|
||||
return await API.request<string>(
|
||||
buildURL(`package/readme/${packageName}${packageVersion ? `?v=${packageVersion}` : ''}`),
|
||||
'GET'
|
||||
);
|
||||
};
|
||||
|
||||
const callDetailPage = async (
|
||||
packageName: string,
|
||||
packageVersion?: string
|
||||
): Promise<PackageMetaInterface> => {
|
||||
return await API.request<PackageMetaInterface>(
|
||||
buildURL(`sidebar/${packageName}${packageVersion ? `?v=${packageVersion}` : ''}`),
|
||||
'GET'
|
||||
);
|
||||
};
|
||||
|
||||
const callSearch = async (value: string, signal: AbortSignal): Promise<string> => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility
|
||||
// FUTURE: signal is not well supported for IE and Samsung Browser
|
||||
return API.request(buildURL(`search/${encodeURIComponent(value)}`), 'GET', {
|
||||
signal,
|
||||
headers: {},
|
||||
});
|
||||
};
|
||||
|
||||
const getPackages = async (): Promise<Package[]> => {
|
||||
return await API.request(buildURL('packages'), 'GET');
|
||||
};
|
||||
|
||||
const doLogin = async (username: string, password: string): Promise<LoginBody> => {
|
||||
return await API.request(buildURL('login'), 'POST', {
|
||||
body: JSON.stringify({ username, password }),
|
||||
headers: {
|
||||
Accept: HEADERS.JSON,
|
||||
'Content-Type': HEADERS.JSON,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getResource = async (link: string): Promise<Blob> => {
|
||||
return 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 value = useMemo(
|
||||
() => ({
|
||||
callReadme,
|
||||
callDetailPage,
|
||||
callSearch,
|
||||
getPackages,
|
||||
doLogin,
|
||||
getResource,
|
||||
}),
|
||||
[callReadme, getResource, callDetailPage, callSearch, doLogin]
|
||||
);
|
||||
|
||||
return <AppAPIContext.Provider value={value}>{children}</AppAPIContext.Provider>;
|
||||
};
|
||||
|
||||
export default APIProvider;
|
||||
|
||||
export const useAPI = () => useContext(AppAPIContext);
|
|
@ -85,6 +85,7 @@ describe('api', () => {
|
|||
credentials: 'same-origin',
|
||||
headers: new Headers({
|
||||
Authorization: 'Bearer token-xx-xx-xx',
|
||||
'x-client': 'verdaccio-ui',
|
||||
}),
|
||||
method: 'GET',
|
||||
});
|
||||
|
|
|
@ -29,6 +29,8 @@ export function handleResponseType(response: Response): Promise<[boolean, any]>
|
|||
return Promise.all([response.ok, response.text()]);
|
||||
}
|
||||
|
||||
const AuthHeader = 'Authorization';
|
||||
|
||||
class API {
|
||||
public request<T>(
|
||||
url: string,
|
||||
|
@ -38,11 +40,13 @@ class API {
|
|||
const token = storage.getItem('token');
|
||||
const headers = new Headers(options.headers);
|
||||
|
||||
if (token && headers.has('Authorization') === false) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
if (token && headers.has(AuthHeader) === false) {
|
||||
headers.set(AuthHeader, `Bearer ${token}`);
|
||||
options.headers = headers;
|
||||
}
|
||||
|
||||
headers.set('x-client', 'verdaccio-ui');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(url, {
|
||||
method,
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { default, useAPI } from './APIProvider';
|
1
packages/plugins/ui-theme/src/store/index.ts
Normal file
1
packages/plugins/ui-theme/src/store/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { store } from './store';
|
42
packages/plugins/ui-theme/src/store/models/configuration.ts
Normal file
42
packages/plugins/ui-theme/src/store/models/configuration.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { createModel } from '@rematch/core';
|
||||
import { Package, TemplateUIOptions } from '@verdaccio/types';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import isNil from 'lodash/isNil';
|
||||
|
||||
import { PRIMARY_COLOR } from 'verdaccio-ui/utils/colors';
|
||||
|
||||
import API from '../../providers/API/api';
|
||||
|
||||
import type { RootModel } from '.';
|
||||
|
||||
const defaultValues: TemplateUIOptions = {
|
||||
primaryColor: PRIMARY_COLOR,
|
||||
darkMode: false,
|
||||
pkgManagers: ['yarn', 'pnpm', 'npm'],
|
||||
scope: '',
|
||||
base: '',
|
||||
login: true,
|
||||
url_prefix: '',
|
||||
title: 'Verdaccio',
|
||||
};
|
||||
|
||||
function getConfiguration() {
|
||||
const uiConfiguration = window?.__VERDACCIO_BASENAME_UI_OPTIONS ?? defaultValues;
|
||||
if (isNil(uiConfiguration.primaryColor) || isEmpty(uiConfiguration.primaryColor)) {
|
||||
uiConfiguration.primaryColor = PRIMARY_COLOR;
|
||||
}
|
||||
|
||||
return uiConfiguration;
|
||||
}
|
||||
|
||||
export const configuration = createModel<RootModel>()({
|
||||
state: {
|
||||
config: getConfiguration(),
|
||||
},
|
||||
effects: (dispatch) => ({
|
||||
async getPackages() {
|
||||
const payload: Package[] = await API.request(`/-/verdaccio/packages`, 'GET');
|
||||
dispatch.packages.savePackages(payload);
|
||||
},
|
||||
}),
|
||||
});
|
31
packages/plugins/ui-theme/src/store/models/download.ts
Normal file
31
packages/plugins/ui-theme/src/store/models/download.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { createModel } from '@rematch/core';
|
||||
|
||||
import API from '../../providers/API/api';
|
||||
import { downloadFile, extractFileName } from '../../utils/url';
|
||||
|
||||
import type { RootModel } from '.';
|
||||
|
||||
export const download = createModel<RootModel>()({
|
||||
state: {},
|
||||
reducers: {},
|
||||
effects: () => ({
|
||||
async getTarball({ link }) {
|
||||
// const basePath = state.configuration.config.base;
|
||||
try {
|
||||
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);
|
||||
} catch (error: any) {
|
||||
// TODO: handle better error
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('error on download', error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
17
packages/plugins/ui-theme/src/store/models/index.ts
Normal file
17
packages/plugins/ui-theme/src/store/models/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Models } from '@rematch/core';
|
||||
|
||||
import { configuration } from './configuration';
|
||||
import { download } from './download';
|
||||
import { login } from './login';
|
||||
import { manifest } from './manifest';
|
||||
import { packages } from './packages';
|
||||
import { search } from './search';
|
||||
export interface RootModel extends Models<RootModel> {
|
||||
packages: typeof packages;
|
||||
manifest: typeof manifest;
|
||||
configuration: typeof configuration;
|
||||
download: typeof download;
|
||||
login: typeof login;
|
||||
search: typeof search;
|
||||
}
|
||||
export const models: RootModel = { packages, configuration, search, download, login, manifest };
|
90
packages/plugins/ui-theme/src/store/models/login.ts
Normal file
90
packages/plugins/ui-theme/src/store/models/login.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { createModel } from '@rematch/core';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import { HEADERS } from '../../lib/constants';
|
||||
import API from '../../providers/API/api';
|
||||
import storage from '../../utils/storage';
|
||||
|
||||
import type { RootModel } from '.';
|
||||
|
||||
export type LoginError = {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type LoginResponse = {
|
||||
username: string | null;
|
||||
token: string | null;
|
||||
};
|
||||
|
||||
export type LoginBody = {
|
||||
error?: LoginError;
|
||||
} & LoginResponse;
|
||||
|
||||
const token = storage.getItem('token');
|
||||
const username = storage.getItem('username');
|
||||
const defaultUserState: LoginBody = {
|
||||
token,
|
||||
username,
|
||||
};
|
||||
|
||||
export const login = createModel<RootModel>()({
|
||||
state: {
|
||||
username: defaultUserState.username,
|
||||
token: defaultUserState.token,
|
||||
},
|
||||
reducers: {
|
||||
logOutUser(state) {
|
||||
storage.removeItem('username');
|
||||
storage.removeItem('token');
|
||||
return {
|
||||
...state,
|
||||
username: null,
|
||||
token: null,
|
||||
};
|
||||
},
|
||||
addError(state, error: LoginError) {
|
||||
return {
|
||||
...state,
|
||||
error,
|
||||
};
|
||||
},
|
||||
clearError(state) {
|
||||
return {
|
||||
...state,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
logInUser(state, response: LoginResponse) {
|
||||
// we might persist this in another way with
|
||||
storage.setItem('username', response.username as string);
|
||||
storage.setItem('token', response.token as string);
|
||||
return {
|
||||
...state,
|
||||
token: response.token as string,
|
||||
username: response.username as string,
|
||||
};
|
||||
},
|
||||
},
|
||||
effects: (dispatch) => ({
|
||||
async getUser({ username, password }, state) {
|
||||
const basePath = state.configuration.config.base;
|
||||
try {
|
||||
const payload: LoginResponse = await API.request(`${basePath}-/verdaccio/login`, 'POST', {
|
||||
body: JSON.stringify({ username, password }),
|
||||
headers: {
|
||||
Accept: HEADERS.JSON,
|
||||
'Content-Type': HEADERS.JSON,
|
||||
},
|
||||
});
|
||||
dispatch.login.logInUser(payload);
|
||||
dispatch.packages.getPackages();
|
||||
} catch (error: any) {
|
||||
dispatch.login.addError({
|
||||
type: 'error',
|
||||
description: i18next.t('form-validation.unable-to-sign-in'),
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
91
packages/plugins/ui-theme/src/store/models/manifest.ts
Normal file
91
packages/plugins/ui-theme/src/store/models/manifest.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import { createModel } from '@rematch/core';
|
||||
import { Package } from '@verdaccio/types';
|
||||
import { PackageMetaInterface } from 'types/packageMeta';
|
||||
|
||||
import API from '../../providers/API/api';
|
||||
|
||||
import type { RootModel } from '.';
|
||||
|
||||
function isPackageVersionValid(
|
||||
packageMeta: Partial<PackageMetaInterface>,
|
||||
packageVersion?: string
|
||||
): boolean {
|
||||
if (!packageVersion || typeof packageVersion === 'undefined') {
|
||||
// if is undefined, that means versions does not exist, we continue
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packageMeta.versions) {
|
||||
return Object.keys(packageMeta.versions).includes(packageVersion);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export const manifest = createModel<RootModel>()({
|
||||
state: {},
|
||||
reducers: {
|
||||
notFound(state) {
|
||||
return {
|
||||
...state,
|
||||
hasNotBeenFound: true,
|
||||
manifest: undefined,
|
||||
packageName: undefined,
|
||||
packageVersion: undefined,
|
||||
readme: undefined,
|
||||
};
|
||||
},
|
||||
clearError(state) {
|
||||
return {
|
||||
...state,
|
||||
isError: null,
|
||||
};
|
||||
},
|
||||
isError(state) {
|
||||
return {
|
||||
...state,
|
||||
isError: true,
|
||||
hasNotBeenFound: false,
|
||||
manifest: undefined,
|
||||
packageName: undefined,
|
||||
packageVersion: undefined,
|
||||
readme: undefined,
|
||||
};
|
||||
},
|
||||
saveManifest(state, { packageName, packageVersion, manifest, readme }) {
|
||||
return {
|
||||
...state,
|
||||
manifest,
|
||||
packageName,
|
||||
packageVersion,
|
||||
readme,
|
||||
hasNotBeenFound: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
effects: (dispatch) => ({
|
||||
async getManifest({ packageName, packageVersion }, state) {
|
||||
const basePath = state.configuration.config.base;
|
||||
try {
|
||||
if (!isPackageVersionValid(packageName, packageVersion)) {
|
||||
throw new Error('not found');
|
||||
}
|
||||
|
||||
const manifest: Package = await API.request(
|
||||
`${basePath}-/verdaccio/sidebar/${packageName}${
|
||||
packageVersion ? `?v=${packageVersion}` : ''
|
||||
}`
|
||||
);
|
||||
const readme: string = await API.request<string>(
|
||||
`${basePath}-/verdaccio/package/readme/${packageName}${
|
||||
packageVersion ? `?v=${packageVersion}` : ''
|
||||
}`,
|
||||
'GET'
|
||||
);
|
||||
dispatch.manifest.saveManifest({ packageName, packageVersion, manifest, readme });
|
||||
} catch (error: any) {
|
||||
dispatch.manifest.notFound();
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
36
packages/plugins/ui-theme/src/store/models/packages.ts
Normal file
36
packages/plugins/ui-theme/src/store/models/packages.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { createModel } from '@rematch/core';
|
||||
import { Package } from '@verdaccio/types';
|
||||
|
||||
import API from '../../providers/API/api';
|
||||
|
||||
import type { RootModel } from '.';
|
||||
|
||||
export const packages = createModel<RootModel>()({
|
||||
state: {
|
||||
response: [] as Package[],
|
||||
},
|
||||
reducers: {
|
||||
savePackages(state, response: Package[]) {
|
||||
return {
|
||||
...state,
|
||||
response,
|
||||
};
|
||||
},
|
||||
},
|
||||
effects: (dispatch) => ({
|
||||
async getPackages(_payload, state) {
|
||||
const basePath = state.configuration.config.base;
|
||||
try {
|
||||
const payload: Package[] = await API.request(`${basePath}-/verdaccio/packages`, 'GET');
|
||||
dispatch.packages.savePackages(payload);
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error({
|
||||
title: 'Warning',
|
||||
message: `Unable to load package list: ${error.message}`,
|
||||
});
|
||||
// TODO: handle error, display something retry or something
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
81
packages/plugins/ui-theme/src/store/models/search.ts
Normal file
81
packages/plugins/ui-theme/src/store/models/search.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { createModel } from '@rematch/core';
|
||||
import { Package } from '@verdaccio/types';
|
||||
|
||||
import API from '../../providers/API/api';
|
||||
|
||||
import type { RootModel } from '.';
|
||||
|
||||
const CONSTANTS = {
|
||||
API_DELAY: 300,
|
||||
ABORT_ERROR: 'AbortError',
|
||||
};
|
||||
|
||||
type SearchState = {
|
||||
suggestions: Partial<Package>[];
|
||||
controller: AbortController[];
|
||||
};
|
||||
|
||||
export const search = createModel<RootModel>()({
|
||||
state: {
|
||||
suggestions: [],
|
||||
controller: [],
|
||||
} as SearchState,
|
||||
reducers: {
|
||||
clearRequestQueue(state) {
|
||||
const controllers = state.controller;
|
||||
controllers.forEach((request) => request.abort());
|
||||
return {
|
||||
...state,
|
||||
controller: [],
|
||||
};
|
||||
},
|
||||
addControllerToQueue(state, { controller }: { controller: AbortController }) {
|
||||
const currentControllers = state.controller;
|
||||
return {
|
||||
...state,
|
||||
controller: [...currentControllers, controller],
|
||||
};
|
||||
},
|
||||
setError(state) {
|
||||
return {
|
||||
...state,
|
||||
isError: true,
|
||||
};
|
||||
},
|
||||
saveSearch(state, { suggestions }: { suggestions: Partial<Package>[] }) {
|
||||
return {
|
||||
...state,
|
||||
suggestions,
|
||||
isError: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
effects: (dispatch) => ({
|
||||
async getSuggestions({ value }, state) {
|
||||
const basePath = state.configuration.config.base;
|
||||
try {
|
||||
const controller = new window.AbortController();
|
||||
dispatch.search.addControllerToQueue({ controller });
|
||||
const signal = controller.signal;
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility
|
||||
// FUTURE: signal is not well supported for IE and Samsung Browser
|
||||
const suggestions: Partial<Package>[] = await API.request(
|
||||
`${basePath}-/verdaccio/search/${encodeURIComponent(value)}`,
|
||||
'GET',
|
||||
{
|
||||
signal,
|
||||
headers: {},
|
||||
}
|
||||
);
|
||||
|
||||
dispatch.search.saveSearch({ suggestions });
|
||||
} catch (error: any) {
|
||||
if (error.name === CONSTANTS.ABORT_ERROR) {
|
||||
dispatch.search.saveSearch({ suggestions: [] });
|
||||
} else {
|
||||
dispatch.search.setError();
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
15
packages/plugins/ui-theme/src/store/store.ts
Normal file
15
packages/plugins/ui-theme/src/store/store.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { init, RematchDispatch, RematchRootState } from '@rematch/core';
|
||||
import loadingPlugin, { ExtraModelsFromLoading } from '@rematch/loading';
|
||||
|
||||
import { models, RootModel } from './models';
|
||||
|
||||
type FullModel = ExtraModelsFromLoading<RootModel>;
|
||||
|
||||
export const store = init<RootModel, FullModel>({
|
||||
models,
|
||||
plugins: [loadingPlugin()],
|
||||
});
|
||||
|
||||
export type Store = typeof store;
|
||||
export type Dispatch = RematchDispatch<RootModel>;
|
||||
export type RootState = RematchRootState<RootModel, FullModel>;
|
|
@ -1,25 +1,38 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import ThemeProvider from 'verdaccio-ui/design-tokens/ThemeProvider';
|
||||
import APIProvider from 'verdaccio-ui/providers/API/APIProvider';
|
||||
import AppConfigurationProvider from 'verdaccio-ui/providers/config';
|
||||
|
||||
import i18nConfig from '../i18n/config';
|
||||
|
||||
const renderWithStore = (ui, store) =>
|
||||
render(ui, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={store}>
|
||||
<AppConfigurationProvider>
|
||||
<ThemeProvider>
|
||||
<I18nextProvider i18n={i18nConfig}>{children}</I18nextProvider>
|
||||
</ThemeProvider>
|
||||
</AppConfigurationProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
|
||||
const customRender = (node: React.ReactElement, ...options: any) => {
|
||||
return render(
|
||||
<AppConfigurationProvider>
|
||||
<APIProvider>
|
||||
<ThemeProvider>
|
||||
<I18nextProvider i18n={i18nConfig}>{node}</I18nextProvider>
|
||||
</ThemeProvider>
|
||||
</APIProvider>
|
||||
<ThemeProvider>
|
||||
<I18nextProvider i18n={i18nConfig}>{node}</I18nextProvider>
|
||||
</ThemeProvider>
|
||||
</AppConfigurationProvider>,
|
||||
...options
|
||||
);
|
||||
};
|
||||
|
||||
export * from '@testing-library/react';
|
||||
// FIXME: rename all references with customRemder
|
||||
export { customRender as render };
|
||||
export { customRender, renderWithStore };
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"verdaccio-ui/utils/*": ["src/utils/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "types/*.d.ts", "src/i18n/**/*.json"],
|
||||
"include": ["src", "types/*.d.ts", "src/i18n/**/*.json", "jest/unit/components"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../node-api"
|
||||
|
|
|
@ -1,26 +1,8 @@
|
|||
// FIXME: this should comes from @verdaccio/types
|
||||
|
||||
type PackageManagers = 'pnpm' | 'yarn' | 'npm';
|
||||
export interface VerdaccioOptions {
|
||||
url_prefix: string;
|
||||
base: string;
|
||||
scope: string;
|
||||
title: string;
|
||||
primaryColor: string;
|
||||
darkMode: boolean;
|
||||
uri?: string;
|
||||
login?: boolean;
|
||||
language?: string;
|
||||
version?: string;
|
||||
protocol?: string;
|
||||
host?: string;
|
||||
logo?: string;
|
||||
pkgManagers?: PackageManagers[];
|
||||
}
|
||||
|
||||
import { TemplateUIOptions } from '@verdaccio/types';
|
||||
declare global {
|
||||
interface Window {
|
||||
__VERDACCIO_BASENAME_UI_OPTIONS: VerdaccioOptions;
|
||||
__VERDACCIO_BASENAME_UI_OPTIONS: TemplateUIOptions;
|
||||
// FIXME: remove all these variables
|
||||
VERDACCIO_PRIMARY_COLOR: string;
|
||||
VERDACCIO_LOGO: string;
|
||||
VERDACCIO_SCOPE: string;
|
||||
|
|
|
@ -25,6 +25,7 @@ const debug = buildDebug('verdaccio:web:api:package');
|
|||
|
||||
function addPackageWebApi(route: Router, storage: Storage, auth: IAuth, config: Config): void {
|
||||
const isLoginEnabled = config?.web?.login === true ?? true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const anonymousRemoteUser: RemoteUser = {
|
||||
name: undefined,
|
||||
real_groups: [],
|
||||
|
@ -35,9 +36,10 @@ function addPackageWebApi(route: Router, storage: Storage, auth: IAuth, config:
|
|||
const checkAllow = (name: string, remoteUser: RemoteUser): Promise<boolean> =>
|
||||
new Promise((resolve, reject): void => {
|
||||
debug('is login disabled %o', isLoginEnabled);
|
||||
const remoteUserAccess = !isLoginEnabled ? anonymousRemoteUser : remoteUser;
|
||||
// FIXME: this logic does not work, review
|
||||
// const remoteUserAccess = !isLoginEnabled ? anonymousRemoteUser : remoteUser;
|
||||
try {
|
||||
auth.allow_access({ packageName: name }, remoteUserAccess, (err, allowed): void => {
|
||||
auth.allow_access({ packageName: name }, remoteUser, (err, allowed): void => {
|
||||
if (err) {
|
||||
resolve(false);
|
||||
}
|
||||
|
|
273
pnpm-lock.yaml
273
pnpm-lock.yaml
|
@ -74,7 +74,6 @@ importers:
|
|||
jest-environment-jsdom: 27.1.0
|
||||
jest-environment-jsdom-global: 3.0.0
|
||||
jest-environment-node: 27.1.0
|
||||
jest-fetch-mock: 3.0.3
|
||||
jest-junit: 12.2.0
|
||||
kleur: 3.0.3
|
||||
lint-staged: 11.1.2
|
||||
|
@ -165,7 +164,6 @@ importers:
|
|||
jest-environment-jsdom: 27.1.0
|
||||
jest-environment-jsdom-global: 3.0.0_jest-environment-jsdom@27.1.0
|
||||
jest-environment-node: 27.1.0
|
||||
jest-fetch-mock: 3.0.3
|
||||
jest-junit: 12.2.0
|
||||
kleur: 3.0.3
|
||||
lint-staged: 11.1.2
|
||||
|
@ -750,15 +748,18 @@ importers:
|
|||
'@material-ui/core': 4.11.4
|
||||
'@material-ui/icons': 4.11.2
|
||||
'@material-ui/styles': 4.11.4
|
||||
'@testing-library/dom': 8.2.0
|
||||
'@rematch/core': 2.1.0
|
||||
'@rematch/loading': 2.1.0
|
||||
'@testing-library/dom': 8.5.0
|
||||
'@testing-library/jest-dom': 5.14.1
|
||||
'@testing-library/react': 12.0.0
|
||||
'@testing-library/react': 12.1.0
|
||||
'@types/react': 17.0.19
|
||||
'@types/react-autosuggest': 10.1.5
|
||||
'@types/react-dom': 17.0.9
|
||||
'@types/react-helmet': 6.1.2
|
||||
'@types/react-router-dom': 5.1.8
|
||||
'@types/react-virtualized': 9.21.13
|
||||
'@types/redux': 3.6.0
|
||||
'@verdaccio/node-api': workspace:6.0.0-6-next.20
|
||||
autosuggest-highlight: 3.1.1
|
||||
babel-loader: 8.2.2
|
||||
|
@ -782,6 +783,7 @@ importers:
|
|||
localstorage-memory: 1.0.3
|
||||
lodash: 4.17.21
|
||||
mini-css-extract-plugin: 2.2.2
|
||||
msw: 0.35.0
|
||||
mutationobserver-shim: 0.3.7
|
||||
node-mocks-http: 1.10.1
|
||||
normalize.css: 8.0.1
|
||||
|
@ -791,12 +793,14 @@ importers:
|
|||
react: 17.0.2
|
||||
react-autosuggest: 10.1.0
|
||||
react-dom: 17.0.2
|
||||
react-hook-form: 7.14.2
|
||||
react-hook-form: 7.15.3
|
||||
react-hot-loader: 4.13.0
|
||||
react-i18next: 11.12.0
|
||||
react-redux: 7.2.1
|
||||
react-router: 5.2.1
|
||||
react-router-dom: 5.3.0
|
||||
react-virtualized: 9.22.3
|
||||
redux: 4.1.1
|
||||
rimraf: 3.0.2
|
||||
standard-version: 9.3.1
|
||||
style-loader: 3.2.1
|
||||
|
@ -826,15 +830,18 @@ importers:
|
|||
'@material-ui/core': 4.11.4_9c6a8df88c2691f81f37725d5b4de033
|
||||
'@material-ui/icons': 4.11.2_842d6fd0a208aabbcab28b4283e0161f
|
||||
'@material-ui/styles': 4.11.4_9c6a8df88c2691f81f37725d5b4de033
|
||||
'@testing-library/dom': 8.2.0
|
||||
'@rematch/core': 2.1.0_redux@4.1.1
|
||||
'@rematch/loading': 2.1.0_@rematch+core@2.1.0
|
||||
'@testing-library/dom': 8.5.0
|
||||
'@testing-library/jest-dom': 5.14.1
|
||||
'@testing-library/react': 12.0.0_react-dom@17.0.2+react@17.0.2
|
||||
'@testing-library/react': 12.1.0_react-dom@17.0.2+react@17.0.2
|
||||
'@types/react': 17.0.19
|
||||
'@types/react-autosuggest': 10.1.5
|
||||
'@types/react-dom': 17.0.9
|
||||
'@types/react-helmet': 6.1.2
|
||||
'@types/react-router-dom': 5.1.8
|
||||
'@types/react-virtualized': 9.21.13
|
||||
'@types/redux': 3.6.0
|
||||
'@verdaccio/node-api': link:../../node-api
|
||||
autosuggest-highlight: 3.1.1
|
||||
babel-loader: 8.2.2_02ab79faf18a98050fd2cd956ffa58f7
|
||||
|
@ -858,6 +865,7 @@ importers:
|
|||
localstorage-memory: 1.0.3
|
||||
lodash: 4.17.21
|
||||
mini-css-extract-plugin: 2.2.2_webpack@5.52.0
|
||||
msw: 0.35.0
|
||||
mutationobserver-shim: 0.3.7
|
||||
node-mocks-http: 1.10.1
|
||||
normalize.css: 8.0.1
|
||||
|
@ -867,12 +875,14 @@ importers:
|
|||
react: 17.0.2
|
||||
react-autosuggest: 10.1.0_react@17.0.2
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
react-hook-form: 7.14.2_react@17.0.2
|
||||
react-hook-form: 7.15.3_react@17.0.2
|
||||
react-hot-loader: 4.13.0_9c6a8df88c2691f81f37725d5b4de033
|
||||
react-i18next: 11.12.0_i18next@20.6.1+react@17.0.2
|
||||
react-redux: 7.2.1_e084f946920a04d85390aa355643c4d5
|
||||
react-router: 5.2.1_react@17.0.2
|
||||
react-router-dom: 5.3.0_react@17.0.2
|
||||
react-virtualized: 9.22.3_react-dom@17.0.2+react@17.0.2
|
||||
redux: 4.1.1
|
||||
rimraf: 3.0.2
|
||||
standard-version: 9.3.1
|
||||
style-loader: 3.2.1_webpack@5.52.0
|
||||
|
@ -5713,6 +5723,26 @@ packages:
|
|||
resolution: {integrity: sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==}
|
||||
dev: false
|
||||
|
||||
/@mswjs/cookies/0.1.6:
|
||||
resolution: {integrity: sha512-A53XD5TOfwhpqAmwKdPtg1dva5wrng2gH5xMvklzbd9WLTSVU953eCRa8rtrrm6G7Cy60BOGsBRN89YQK0mlKA==}
|
||||
dependencies:
|
||||
'@types/set-cookie-parser': 2.4.1
|
||||
set-cookie-parser: 2.4.8
|
||||
dev: true
|
||||
|
||||
/@mswjs/interceptors/0.12.7:
|
||||
resolution: {integrity: sha512-eGjZ3JRAt0Fzi5FgXiV/P3bJGj0NqsN7vBS0J0FO2AQRQ0jCKQS4lEFm4wvlSgKQNfeuc/Vz6d81VtU3Gkx/zg==}
|
||||
dependencies:
|
||||
'@open-draft/until': 1.0.3
|
||||
'@xmldom/xmldom': 0.7.5
|
||||
debug: 4.3.2
|
||||
headers-utils: 3.0.2
|
||||
outvariant: 1.2.1
|
||||
strict-event-emitter: 0.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.2:
|
||||
resolution: {integrity: sha512-Fb8WxUFOBQVl+CX4MWet5o7eCc6Pj04rXIwVKZ6h1NnqTo45eOQW6aWyhG25NIODvWFwTDMwBsYxrQ3imxpetg==}
|
||||
requiresBuild: true
|
||||
|
@ -5749,6 +5779,10 @@ packages:
|
|||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.11.0
|
||||
|
||||
/@open-draft/until/1.0.3:
|
||||
resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==}
|
||||
dev: true
|
||||
|
||||
/@polka/url/1.0.0-next.15:
|
||||
resolution: {integrity: sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA==}
|
||||
|
||||
|
@ -5795,6 +5829,23 @@ packages:
|
|||
resolution: {integrity: sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=}
|
||||
dev: false
|
||||
|
||||
/@rematch/core/2.1.0_redux@4.1.1:
|
||||
resolution: {integrity: sha512-izr4LlXsHp1gfK8v05FNvqSthX64cCj59/x3tu+3qNXSuSM7d1/YXAlUtBJIoX9RApr+d1mqA7CCAteUjZrdLg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
redux: '>=4'
|
||||
dependencies:
|
||||
redux: 4.1.1
|
||||
dev: true
|
||||
|
||||
/@rematch/loading/2.1.0_@rematch+core@2.1.0:
|
||||
resolution: {integrity: sha512-4sLWHiuTW50zjdbhUVHb9HB9Ors6+ktM9tANBOinYu4c43TSArZLkaC7lb8fZYLb6eCsHHAtrrWYsz8lBJ6UOw==}
|
||||
peerDependencies:
|
||||
'@rematch/core': '>=2'
|
||||
dependencies:
|
||||
'@rematch/core': 2.1.0_redux@4.1.1
|
||||
dev: true
|
||||
|
||||
/@sideway/address/4.1.0:
|
||||
resolution: {integrity: sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==}
|
||||
dependencies:
|
||||
|
@ -5974,8 +6025,8 @@ packages:
|
|||
dependencies:
|
||||
defer-to-connect: 1.1.3
|
||||
|
||||
/@testing-library/dom/8.2.0:
|
||||
resolution: {integrity: sha512-U8cTWENQPHO3QHvxBdfltJ+wC78ytMdg69ASvIdkGdQ/XRg4M9H2vvM3mHddxl+w/fM6NNqzGMwpQoh82v9VIA==}
|
||||
/@testing-library/dom/8.5.0:
|
||||
resolution: {integrity: sha512-O0fmHFaPlqaYCpa/cBL0cvroMridb9vZsMLacgIqrlxj+fd+bGF8UfAgwsLCHRF84KLBafWlm9CuOvxeNTlodw==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.14.5
|
||||
|
@ -5985,7 +6036,7 @@ packages:
|
|||
chalk: 4.1.1
|
||||
dom-accessibility-api: 0.5.6
|
||||
lz-string: 1.4.4
|
||||
pretty-format: 27.0.2
|
||||
pretty-format: 27.1.0
|
||||
dev: true
|
||||
|
||||
/@testing-library/jest-dom/5.14.1:
|
||||
|
@ -6003,15 +6054,15 @@ packages:
|
|||
redent: 3.0.0
|
||||
dev: true
|
||||
|
||||
/@testing-library/react/12.0.0_react-dom@17.0.2+react@17.0.2:
|
||||
resolution: {integrity: sha512-sh3jhFgEshFyJ/0IxGltRhwZv2kFKfJ3fN1vTZ6hhMXzz9ZbbcTgmDYM4e+zJv+oiVKKEWZPyqPAh4MQBI65gA==}
|
||||
/@testing-library/react/12.1.0_react-dom@17.0.2+react@17.0.2:
|
||||
resolution: {integrity: sha512-Ge3Ht3qXE82Yv9lyPpQ7ZWgzo/HgOcHu569Y4ZGWcZME38iOFiOg87qnu6hTEa8jTJVL7zYovnvD3GE2nsNIoQ==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
dependencies:
|
||||
'@babel/runtime': 7.15.4
|
||||
'@testing-library/dom': 8.2.0
|
||||
'@testing-library/dom': 8.5.0
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
dev: true
|
||||
|
@ -6118,6 +6169,10 @@ packages:
|
|||
'@types/node': 16.9.1
|
||||
dev: true
|
||||
|
||||
/@types/cookie/0.4.1:
|
||||
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
||||
dev: true
|
||||
|
||||
/@types/cookiejar/2.1.2:
|
||||
resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==}
|
||||
dev: true
|
||||
|
@ -6190,6 +6245,13 @@ packages:
|
|||
resolution: {integrity: sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==}
|
||||
dev: true
|
||||
|
||||
/@types/inquirer/7.3.3:
|
||||
resolution: {integrity: sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ==}
|
||||
dependencies:
|
||||
'@types/through': 0.0.30
|
||||
rxjs: 6.6.7
|
||||
dev: true
|
||||
|
||||
/@types/istanbul-lib-coverage/2.0.3:
|
||||
resolution: {integrity: sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==}
|
||||
dev: true
|
||||
|
@ -6226,6 +6288,10 @@ packages:
|
|||
pretty-format: 27.0.2
|
||||
dev: true
|
||||
|
||||
/@types/js-levenshtein/1.1.0:
|
||||
resolution: {integrity: sha512-14t0v1ICYRtRVcHASzes0v/O+TIeASb8aD55cWF1PidtInhFWSXcmhzhHqGjUWf9SUq1w70cvd1cWKUULubAfQ==}
|
||||
dev: true
|
||||
|
||||
/@types/json-schema/7.0.6:
|
||||
resolution: {integrity: sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==}
|
||||
|
||||
|
@ -6432,6 +6498,13 @@ packages:
|
|||
csstype: 3.0.8
|
||||
dev: true
|
||||
|
||||
/@types/redux/3.6.0:
|
||||
resolution: {integrity: sha1-8evh5UEVGAcuT9/KXHbhbnTBOZo=}
|
||||
deprecated: This is a stub types definition for Redux (https://github.com/reactjs/redux). Redux provides its own type definitions, so you don't need @types/redux installed!
|
||||
dependencies:
|
||||
redux: 4.1.1
|
||||
dev: true
|
||||
|
||||
/@types/request/2.48.7:
|
||||
resolution: {integrity: sha512-GWP9AZW7foLd4YQxyFZDBepl0lPsWLMEXDZUjQ/c1gqVPDPECrRZyEzuhJdnPWioFCq3Tv0qoGpMD6U+ygd4ZA==}
|
||||
dependencies:
|
||||
|
@ -6466,6 +6539,12 @@ packages:
|
|||
'@types/node': 16.9.1
|
||||
dev: true
|
||||
|
||||
/@types/set-cookie-parser/2.4.1:
|
||||
resolution: {integrity: sha512-N0IWe4vT1w5IOYdN9c9PNpQniHS+qe25W4tj4vfhJDJ9OkvA/YA55YUhaC+HNmMMeLlOSnBW9UMno0qlt5xu3Q==}
|
||||
dependencies:
|
||||
'@types/node': 16.9.1
|
||||
dev: true
|
||||
|
||||
/@types/sonic-boom/2.1.1:
|
||||
resolution: {integrity: sha512-CiKn+8CDgtBspfAVPwC8PXCMPhqeL7pFS4aWuj+WJnHLZlu4OGPctdZ6Mob43jRe0kkd7Ztb2Hcu9kzB+b7ZFw==}
|
||||
deprecated: This is a stub types definition. sonic-boom provides its own type definitions, so you do not need this installed.
|
||||
|
@ -6509,6 +6588,12 @@ packages:
|
|||
'@types/jest': 26.0.19
|
||||
dev: true
|
||||
|
||||
/@types/through/0.0.30:
|
||||
resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==}
|
||||
dependencies:
|
||||
'@types/node': 16.9.1
|
||||
dev: true
|
||||
|
||||
/@types/tough-cookie/4.0.0:
|
||||
resolution: {integrity: sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==}
|
||||
dev: true
|
||||
|
@ -7032,6 +7117,11 @@ packages:
|
|||
webpack-cli: 4.8.0_3691794a826a95bb278217ad314163a5
|
||||
dev: true
|
||||
|
||||
/@xmldom/xmldom/0.7.5:
|
||||
resolution: {integrity: sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
dev: true
|
||||
|
||||
/@xtuc/ieee754/1.2.0:
|
||||
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
||||
|
||||
|
@ -8547,6 +8637,11 @@ packages:
|
|||
string-width: 4.2.2
|
||||
dev: true
|
||||
|
||||
/cli-width/3.0.0:
|
||||
resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
|
||||
engines: {node: '>= 10'}
|
||||
dev: true
|
||||
|
||||
/clipanion/3.0.1:
|
||||
resolution: {integrity: sha512-/ujK3YJ1MGjGr18w99Gl9XZjy4xcC/5bZRJXsgvYG6GbUTO4CTKriC+oUxDbo8G+G/dxDqSJhm8QIDnK6iH6Ig==}
|
||||
dependencies:
|
||||
|
@ -9018,6 +9113,11 @@ packages:
|
|||
resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
/cookie/0.4.1:
|
||||
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/cookiejar/2.1.2:
|
||||
resolution: {integrity: sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==}
|
||||
|
||||
|
@ -9169,6 +9269,7 @@ packages:
|
|||
resolution: {integrity: sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==}
|
||||
dependencies:
|
||||
node-fetch: 2.6.1
|
||||
dev: false
|
||||
|
||||
/cross-spawn/5.1.0:
|
||||
resolution: {integrity: sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=}
|
||||
|
@ -10732,6 +10833,11 @@ packages:
|
|||
resolution: {integrity: sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
|
||||
/events/3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
dev: true
|
||||
|
||||
/eventsource/1.0.7:
|
||||
resolution: {integrity: sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
@ -11874,6 +11980,11 @@ packages:
|
|||
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
||||
dev: true
|
||||
|
||||
/graphql/15.5.3:
|
||||
resolution: {integrity: sha512-sM+jXaO5KinTui6lbK/7b7H/Knj9BpjGxZ+Ki35v7YbUJxxdBCUqNM0h3CRVU1ZF9t5lNiBzvBCSYPvIwxPOQA==}
|
||||
engines: {node: '>= 10.x'}
|
||||
dev: true
|
||||
|
||||
/gray-matter/4.0.3:
|
||||
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
@ -12144,6 +12255,10 @@ packages:
|
|||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
/headers-utils/3.0.2:
|
||||
resolution: {integrity: sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ==}
|
||||
dev: true
|
||||
|
||||
/hex-color-regex/1.1.0:
|
||||
resolution: {integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==}
|
||||
|
||||
|
@ -12594,6 +12709,26 @@ packages:
|
|||
resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
|
||||
dev: false
|
||||
|
||||
/inquirer/8.1.5:
|
||||
resolution: {integrity: sha512-G6/9xUqmt/r+UvufSyrPpt84NYwhKZ9jLsgMbQzlx804XErNupor8WQdBnBRrXmBfTPpuwf1sV+ss2ovjgdXIg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dependencies:
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.1
|
||||
cli-cursor: 3.1.0
|
||||
cli-width: 3.0.0
|
||||
external-editor: 3.1.0
|
||||
figures: 3.2.0
|
||||
lodash: 4.17.21
|
||||
mute-stream: 0.0.8
|
||||
ora: 5.4.1
|
||||
run-async: 2.4.1
|
||||
rxjs: 7.3.0
|
||||
string-width: 4.2.2
|
||||
strip-ansi: 6.0.0
|
||||
through: 2.3.8
|
||||
dev: true
|
||||
|
||||
/internal-ip/4.3.0:
|
||||
resolution: {integrity: sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -12864,6 +12999,10 @@ packages:
|
|||
resolution: {integrity: sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
/is-node-process/1.0.1:
|
||||
resolution: {integrity: sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==}
|
||||
dev: true
|
||||
|
||||
/is-npm/4.0.0:
|
||||
resolution: {integrity: sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -13300,13 +13439,6 @@ packages:
|
|||
jest-util: 27.1.0
|
||||
dev: true
|
||||
|
||||
/jest-fetch-mock/3.0.3:
|
||||
resolution: {integrity: sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==}
|
||||
dependencies:
|
||||
cross-fetch: 3.0.6
|
||||
promise-polyfill: 8.2.0
|
||||
dev: true
|
||||
|
||||
/jest-get-type/26.3.0:
|
||||
resolution: {integrity: sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==}
|
||||
engines: {node: '>= 10.14.2'}
|
||||
|
@ -13677,6 +13809,11 @@ packages:
|
|||
resolution: {integrity: sha512-Frdq2+tRRGLQUIQOgsIGSCd1VePCS2fsddTG5dTCqR0JHgltXWfsxnY0gIXPoMeRmdom6Oyq+UMOFg5suduOjQ==}
|
||||
dev: true
|
||||
|
||||
/js-levenshtein/1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/js-tokens/4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
|
@ -15028,6 +15165,35 @@ packages:
|
|||
/ms/2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
/msw/0.35.0:
|
||||
resolution: {integrity: sha512-V7A6PqaS31F1k//fPS0OnO7vllfaqBUFsMEu3IpYixyWpiUInfyglodnbXhhtDyytkQikpkPZv8TZi/CvZzv/w==}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@mswjs/cookies': 0.1.6
|
||||
'@mswjs/interceptors': 0.12.7
|
||||
'@open-draft/until': 1.0.3
|
||||
'@types/cookie': 0.4.1
|
||||
'@types/inquirer': 7.3.3
|
||||
'@types/js-levenshtein': 1.1.0
|
||||
chalk: 4.1.1
|
||||
chokidar: 3.5.1
|
||||
cookie: 0.4.1
|
||||
graphql: 15.5.3
|
||||
headers-utils: 3.0.2
|
||||
inquirer: 8.1.5
|
||||
is-node-process: 1.0.1
|
||||
js-levenshtein: 1.1.6
|
||||
node-fetch: 2.6.1
|
||||
node-match-path: 0.6.3
|
||||
statuses: 2.0.1
|
||||
strict-event-emitter: 0.2.0
|
||||
type-fest: 1.4.0
|
||||
yargs: 17.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/multicast-dns-service-types/1.1.0:
|
||||
resolution: {integrity: sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=}
|
||||
|
||||
|
@ -15042,6 +15208,10 @@ packages:
|
|||
resolution: {integrity: sha512-oRIDTyZQU96nAiz2AQyngwx1e89iApl2hN5AOYwyxLUB47UYsU3Wv9lJWqH5y/QdiYkc5HQLi23ZNB3fELdHcQ==}
|
||||
dev: true
|
||||
|
||||
/mute-stream/0.0.8:
|
||||
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
|
||||
dev: true
|
||||
|
||||
/mv/2.1.1:
|
||||
resolution: {integrity: sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
@ -15195,6 +15365,10 @@ packages:
|
|||
resolution: {integrity: sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=}
|
||||
dev: true
|
||||
|
||||
/node-match-path/0.6.3:
|
||||
resolution: {integrity: sha512-fB1reOHKLRZCJMAka28hIxCwQLxGmd7WewOCBDYKpyA1KXi68A7vaGgdZAPhY2E6SXoYt3KqYCCvXLJ+O0Fu/Q==}
|
||||
dev: true
|
||||
|
||||
/node-mocks-http/1.10.1:
|
||||
resolution: {integrity: sha512-/Nz83kiJ3z+vGqxmlDyv8+L1CJno+gH23DzG3oPH9dBSfMYa5IFVwPgZpXCB2kdiiIu/HoDpZ2BuLqQs7qjFLQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
@ -15679,6 +15853,10 @@ packages:
|
|||
resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
|
||||
dev: true
|
||||
|
||||
/outvariant/1.2.1:
|
||||
resolution: {integrity: sha512-bcILvFkvpMXh66+Ubax/inxbKRyWTUiiFIW2DWkiS79wakrLGn3Ydy+GvukadiyfZjaL6C7YhIem4EZSM282wA==}
|
||||
dev: true
|
||||
|
||||
/p-cancelable/1.1.0:
|
||||
resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -16824,10 +17002,6 @@ packages:
|
|||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/promise-polyfill/8.2.0:
|
||||
resolution: {integrity: sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==}
|
||||
dev: true
|
||||
|
||||
/promise/7.3.1:
|
||||
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
|
||||
dependencies:
|
||||
|
@ -17134,8 +17308,8 @@ packages:
|
|||
react-side-effect: 2.1.1_react@17.0.2
|
||||
dev: false
|
||||
|
||||
/react-hook-form/7.14.2_react@17.0.2:
|
||||
resolution: {integrity: sha512-32uvgKkaE/0vOncfnJdwQhfahhocPpcb5c7F4j9Eq7dOnqS2Hg8h70Bmt6KXb6veLSWJultc1+ik9QSfqXFmLA==}
|
||||
/react-hook-form/7.15.3_react@17.0.2:
|
||||
resolution: {integrity: sha512-z30aZoEHkWE8oZvad4OcYSBI0kQua/T5sFGH9tB2HfeykFnP/pGXNap8lDio4/U1yPj2ffpbvRIvqKd/6jjBVA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17
|
||||
dependencies:
|
||||
|
@ -17241,6 +17415,29 @@ packages:
|
|||
react: 17.0.2
|
||||
dev: false
|
||||
|
||||
/react-redux/7.2.1_e084f946920a04d85390aa355643c4d5:
|
||||
resolution: {integrity: sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==}
|
||||
peerDependencies:
|
||||
react: ^16.8.3
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
redux: ^2.0.0 || ^3.0.0 || ^4.0.0-0
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.15.4
|
||||
hoist-non-react-statics: 3.3.2
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.7.2
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
react-is: 16.13.1
|
||||
redux: 4.1.1
|
||||
dev: true
|
||||
|
||||
/react-router-config/5.1.1_react-router@5.2.0+react@17.0.2:
|
||||
resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==}
|
||||
peerDependencies:
|
||||
|
@ -17514,6 +17711,12 @@ packages:
|
|||
strip-indent: 3.0.0
|
||||
dev: true
|
||||
|
||||
/redux/4.1.1:
|
||||
resolution: {integrity: sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.15.4
|
||||
dev: true
|
||||
|
||||
/regenerate-unicode-properties/8.2.0:
|
||||
resolution: {integrity: sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -17986,6 +18189,11 @@ packages:
|
|||
strip-json-comments: 3.1.1
|
||||
dev: false
|
||||
|
||||
/run-async/2.4.1:
|
||||
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
dev: true
|
||||
|
||||
/run-parallel/1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
dependencies:
|
||||
|
@ -18699,6 +18907,11 @@ packages:
|
|||
resolution: {integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
/statuses/2.0.1:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/std-env/2.3.0:
|
||||
resolution: {integrity: sha512-4qT5B45+Kjef2Z6pE0BkskzsH0GO7GrND0wGlTM1ioUe3v0dGYx9ZJH0Aro/YyA8fqQ5EyIKDRjZojJYMFTflw==}
|
||||
dependencies:
|
||||
|
@ -18730,6 +18943,12 @@ packages:
|
|||
mixme: 0.4.0
|
||||
dev: true
|
||||
|
||||
/strict-event-emitter/0.2.0:
|
||||
resolution: {integrity: sha512-zv7K2egoKwkQkZGEaH8m+i2D0XiKzx5jNsiSul6ja2IYFvil10A59Z9Y7PPAAe5OW53dQUf9CfsHKzjZzKkm1w==}
|
||||
dependencies:
|
||||
events: 3.3.0
|
||||
dev: true
|
||||
|
||||
/string-argv/0.3.1:
|
||||
resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==}
|
||||
engines: {node: '>=0.6.19'}
|
||||
|
|
|
@ -103,7 +103,7 @@ describe('/ (Verdaccio Page)', () => {
|
|||
const accountButton = await page.$('#header--button-account');
|
||||
expect(accountButton).toBeDefined();
|
||||
// check whether user is logged
|
||||
const buttonLogout = await page.$('#header--button-logout');
|
||||
const buttonLogout = await page.$('#logOutDialogIcon');
|
||||
expect(buttonLogout).toBeDefined();
|
||||
});
|
||||
|
||||
|
@ -113,7 +113,7 @@ describe('/ (Verdaccio Page)', () => {
|
|||
// we assume the user is logged already
|
||||
await clickElement('#header--button-account', { delay: 500 });
|
||||
await page.waitForTimeout(1000);
|
||||
await clickElement('#header--button-logout > span', { delay: 500 });
|
||||
await clickElement('#logOutDialogIcon > span', { delay: 500 });
|
||||
await page.waitForTimeout(1000);
|
||||
await evaluateSignIn();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue