forked from sombochea/verdaccio-ui
feat: login Dialog Component - Replaced class by func. comp + added react-hook-form (#341)
* refactor: convert class to func * refactor: changed login form logic * refactor: conver to testing-library tests * refactor: moved dependency * refactor: replaced uglifyjs-webpack-plugin by terser-webpack-plugin * fix: fixed e2e errors * fix: fixed e2e test * Delete settings.json * fix: vscode settings rollback * refactor: rollback webpack config * refactor: updated eslint rule * fix: removed --fix * refactor: incresed the bundle size
This commit is contained in:
parent
501845b5f8
commit
42d3bb8508
@ -72,7 +72,7 @@
|
|||||||
"arrow": "parens",
|
"arrow": "parens",
|
||||||
"condition": "parens",
|
"condition": "parens",
|
||||||
"logical": "parens",
|
"logical": "parens",
|
||||||
"prop": "parens"
|
"prop": "ignore"
|
||||||
}],
|
}],
|
||||||
"react/jsx-boolean-value": ["error", "always"],
|
"react/jsx-boolean-value": ["error", "always"],
|
||||||
"react/jsx-closing-tag-location": ["error"],
|
"react/jsx-closing-tag-location": ["error"],
|
||||||
@ -83,7 +83,7 @@
|
|||||||
"react/jsx-indent": ["error", 2],
|
"react/jsx-indent": ["error", 2],
|
||||||
"react/jsx-indent-props": ["error", 2],
|
"react/jsx-indent-props": ["error", 2],
|
||||||
"react/jsx-key": ["error"],
|
"react/jsx-key": ["error"],
|
||||||
"react/jsx-max-depth": ["error", { "max": 2}],
|
"react/jsx-max-depth":["error", { "max": 5}],
|
||||||
"react/jsx-max-props-per-line": ["error", {"maximum": 3, "when": "multiline" }],
|
"react/jsx-max-props-per-line": ["error", {"maximum": 3, "when": "multiline" }],
|
||||||
"react/jsx-no-bind": ["error"],
|
"react/jsx-no-bind": ["error"],
|
||||||
"react/jsx-no-comment-textnodes": ["error"],
|
"react/jsx-no-comment-textnodes": ["error"],
|
||||||
|
@ -6,6 +6,7 @@ import 'raf/polyfill';
|
|||||||
import { configure } from 'enzyme';
|
import { configure } from 'enzyme';
|
||||||
import Adapter from 'enzyme-adapter-react-16';
|
import Adapter from 'enzyme-adapter-react-16';
|
||||||
import { GlobalWithFetchMock } from 'jest-fetch-mock';
|
import { GlobalWithFetchMock } from 'jest-fetch-mock';
|
||||||
|
import 'mutationobserver-shim';
|
||||||
|
|
||||||
// @ts-ignore : Only a void function can be called with the 'new' keyword
|
// @ts-ignore : Only a void function can be called with the 'new' keyword
|
||||||
configure({ adapter: new Adapter() });
|
configure({ adapter: new Adapter() });
|
||||||
|
@ -86,6 +86,7 @@
|
|||||||
"lockfile-lint": "3.0.3",
|
"lockfile-lint": "3.0.3",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"mini-css-extract-plugin": "0.8.0",
|
"mini-css-extract-plugin": "0.8.0",
|
||||||
|
"mutationobserver-shim": "0.3.3",
|
||||||
"node-mocks-http": "1.8.0",
|
"node-mocks-http": "1.8.0",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||||
@ -96,6 +97,7 @@
|
|||||||
"react": "16.12.0",
|
"react": "16.12.0",
|
||||||
"react-autosuggest": "9.4.3",
|
"react-autosuggest": "9.4.3",
|
||||||
"react-dom": "16.12.0",
|
"react-dom": "16.12.0",
|
||||||
|
"react-hook-form": "3.28.12",
|
||||||
"react-hot-loader": "4.12.18",
|
"react-hot-loader": "4.12.18",
|
||||||
"react-router-dom": "5.1.2",
|
"react-router-dom": "5.1.2",
|
||||||
"request": "2.88.0",
|
"request": "2.88.0",
|
||||||
@ -136,7 +138,7 @@
|
|||||||
"bundlesize": [
|
"bundlesize": [
|
||||||
{
|
{
|
||||||
"path": "./static/vendors.*.js",
|
"path": "./static/vendors.*.js",
|
||||||
"maxSize": "180 kB"
|
"maxSize": "185 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./static/main.*.js",
|
"path": "./static/main.*.js",
|
||||||
@ -213,6 +215,5 @@
|
|||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/verdaccio",
|
"url": "https://opencollective.com/verdaccio",
|
||||||
"logo": "https://opencollective.com/verdaccio/logo.txt"
|
"logo": "https://opencollective.com/verdaccio/logo.txt"
|
||||||
},
|
}
|
||||||
"dependencies": {}
|
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,6 @@ const StyledBoxContent = styled(Box)({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable react/jsx-max-depth */
|
|
||||||
/* eslint-disable react/jsx-no-bind */
|
/* eslint-disable react/jsx-no-bind */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
import { FormError } from '../components/Login/Login';
|
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
error?: FormError;
|
|
||||||
user?: User;
|
user?: User;
|
||||||
scope: string;
|
scope: string;
|
||||||
packages: any[];
|
packages: any[];
|
||||||
@ -15,7 +12,6 @@ export interface User {
|
|||||||
|
|
||||||
export interface AppContextProps extends AppProps {
|
export interface AppContextProps extends AppProps {
|
||||||
setUser: (user?: User) => void;
|
setUser: (user?: User) => void;
|
||||||
setError: (error?: FormError) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppContext = createContext<undefined | AppContextProps>(undefined);
|
const AppContext = createContext<undefined | AppContextProps>(undefined);
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import { FormError } from '../components/Login/Login';
|
|
||||||
|
|
||||||
import AppContext, { AppProps, User } from './AppContext';
|
import AppContext, { AppProps, User } from './AppContext';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -38,19 +36,11 @@ const AppContextProvider: React.FC<Props> = ({ children, packages, user }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setError = (error?: FormError) => {
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider
|
<AppContext.Provider
|
||||||
value={{
|
value={{
|
||||||
...state,
|
...state,
|
||||||
setUser,
|
setUser,
|
||||||
setError,
|
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
|
@ -23,7 +23,6 @@ export const history = createBrowserHistory({
|
|||||||
basename: window.__VERDACCIO_BASENAME_UI_OPTIONS && window.__VERDACCIO_BASENAME_UI_OPTIONS.url_prefix,
|
basename: window.__VERDACCIO_BASENAME_UI_OPTIONS && window.__VERDACCIO_BASENAME_UI_OPTIONS.url_prefix,
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint react/jsx-max-depth: 0 */
|
|
||||||
const AppRoute: React.FC = () => {
|
const AppRoute: React.FC = () => {
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ export interface ActionBarActionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable react/jsx-no-bind */
|
/* eslint-disable react/jsx-no-bind */
|
||||||
/* eslint-disable react/jsx-max-depth */
|
|
||||||
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
|
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'VISIT_HOMEPAGE':
|
case 'VISIT_HOMEPAGE':
|
||||||
|
@ -10,8 +10,6 @@ import { StyledText, DistListItem, DistChips } from './styles';
|
|||||||
const DistChip: FC<{ name: string }> = ({ name, children }) =>
|
const DistChip: FC<{ name: string }> = ({ name, children }) =>
|
||||||
children ? (
|
children ? (
|
||||||
<DistChips
|
<DistChips
|
||||||
// lint rule conflicting with prettier
|
|
||||||
/* eslint-disable react/jsx-wrap-multilines */
|
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<b>{name}</b>
|
<b>{name}</b>
|
||||||
@ -19,7 +17,6 @@ const DistChip: FC<{ name: string }> = ({ name, children }) =>
|
|||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/* eslint-enable */
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ const Engine: React.FC = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable react/jsx-max-depth */
|
|
||||||
return (
|
return (
|
||||||
<Grid container={true}>
|
<Grid container={true}>
|
||||||
{engines.node && (
|
{engines.node && (
|
||||||
@ -45,7 +44,6 @@ const Engine: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
/* eslint-enable react/jsx-max-depth */
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Engine;
|
export default Engine;
|
||||||
|
@ -44,7 +44,7 @@ describe('<Header /> component with logged in state', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should open login dialog', async () => {
|
test('should open login dialog', async () => {
|
||||||
const { getByText, getByTestId } = render(
|
const { getByText } = render(
|
||||||
<Router>
|
<Router>
|
||||||
<AppContextProvider packages={props.packages}>
|
<AppContextProvider packages={props.packages}>
|
||||||
<Header />
|
<Header />
|
||||||
@ -54,9 +54,8 @@ describe('<Header /> component with logged in state', () => {
|
|||||||
|
|
||||||
const loginBtn = getByText('Login');
|
const loginBtn = getByText('Login');
|
||||||
fireEvent.click(loginBtn);
|
fireEvent.click(loginBtn);
|
||||||
// wait for login modal appearance and return the element
|
const loginDialog = await waitForElement(() => getByText('Sign in'));
|
||||||
const registrationInfoModal = await waitForElement(() => getByTestId('login--form-container'));
|
expect(loginDialog).toBeTruthy();
|
||||||
expect(registrationInfoModal).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should logout the user', async () => {
|
test('should logout the user', async () => {
|
||||||
|
@ -2,10 +2,9 @@ import React, { useState, useContext } from 'react';
|
|||||||
|
|
||||||
import storage from '../../utils/storage';
|
import storage from '../../utils/storage';
|
||||||
import { getRegistryURL } from '../../utils/url';
|
import { getRegistryURL } from '../../utils/url';
|
||||||
import { makeLogin } from '../../utils/login';
|
|
||||||
import Button from '../../muiComponents/Button';
|
import Button from '../../muiComponents/Button';
|
||||||
import AppContext from '../../App/AppContext';
|
import AppContext from '../../App/AppContext';
|
||||||
import LoginModal from '../Login';
|
import LoginDialog from '../LoginDialog';
|
||||||
import Search from '../Search';
|
import Search from '../Search';
|
||||||
|
|
||||||
import { NavBar, InnerNavBar, MobileNavBar, InnerMobileNavBar } from './styles';
|
import { NavBar, InnerNavBar, MobileNavBar, InnerMobileNavBar } from './styles';
|
||||||
@ -17,7 +16,6 @@ interface Props {
|
|||||||
withoutSearch?: boolean;
|
withoutSearch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable react/jsx-max-depth */
|
|
||||||
/* eslint-disable react/jsx-no-bind*/
|
/* eslint-disable react/jsx-no-bind*/
|
||||||
const Header: React.FC<Props> = ({ withoutSearch }) => {
|
const Header: React.FC<Props> = ({ withoutSearch }) => {
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
@ -29,29 +27,9 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
|
|||||||
throw Error('The app Context was not correct used');
|
throw Error('The app Context was not correct used');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user, scope, error, setUser, setError } = appContext;
|
const { user, scope, setUser } = appContext;
|
||||||
const logo = window.VERDACCIO_LOGO;
|
const logo = window.VERDACCIO_LOGO;
|
||||||
|
|
||||||
/**
|
|
||||||
* handles login
|
|
||||||
* Required by: <Header />
|
|
||||||
*/
|
|
||||||
const handleDoLogin = async (usernameValue: string, passwordValue: string) => {
|
|
||||||
const { username, token, error } = await makeLogin(usernameValue, passwordValue);
|
|
||||||
|
|
||||||
if (username && token) {
|
|
||||||
storage.setItem('username', username);
|
|
||||||
storage.setItem('token', token);
|
|
||||||
setUser({ username });
|
|
||||||
setShowLoginModal(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
setUser(undefined);
|
|
||||||
setError(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logouts user
|
* Logouts user
|
||||||
* Required by: <Header />
|
* Required by: <Header />
|
||||||
@ -93,12 +71,7 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</MobileNavBar>
|
</MobileNavBar>
|
||||||
)}
|
)}
|
||||||
<LoginModal
|
{!user && <LoginDialog onClose={() => setShowLoginModal(false)} open={showLoginModal} />}
|
||||||
error={error}
|
|
||||||
onCancel={() => setShowLoginModal(false)}
|
|
||||||
onSubmit={handleDoLogin}
|
|
||||||
visibility={showLoginModal}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,6 @@ interface Props {
|
|||||||
onLoggedInMenuClose: () => void;
|
onLoggedInMenuClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable react/jsx-max-depth */
|
|
||||||
const HeaderMenu: React.FC<Props> = ({
|
const HeaderMenu: React.FC<Props> = ({
|
||||||
onLogout,
|
onLogout,
|
||||||
username,
|
username,
|
||||||
|
@ -1,127 +0,0 @@
|
|||||||
/**
|
|
||||||
* @prettier
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { mount } from '../../utils/test-enzyme';
|
|
||||||
|
|
||||||
import LoginModal from './Login';
|
|
||||||
|
|
||||||
const eventUsername = {
|
|
||||||
target: {
|
|
||||||
value: 'xyz',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventPassword = {
|
|
||||||
target: {
|
|
||||||
value: '1234',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
preventDefault: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('<LoginModal />', () => {
|
|
||||||
test('should load the component in default state', () => {
|
|
||||||
const wrapper = mount(<LoginModal />);
|
|
||||||
expect(wrapper.html()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should load the component with props', () => {
|
|
||||||
const props = {
|
|
||||||
visibility: true,
|
|
||||||
error: {
|
|
||||||
type: 'error',
|
|
||||||
title: 'Error Title',
|
|
||||||
description: 'Error Description',
|
|
||||||
},
|
|
||||||
onCancel: () => {},
|
|
||||||
onSubmit: () => {},
|
|
||||||
};
|
|
||||||
const wrapper = mount(<LoginModal {...props} />);
|
|
||||||
expect(wrapper.html()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onCancel: should close the login modal', () => {
|
|
||||||
const props = {
|
|
||||||
visibility: true,
|
|
||||||
error: {
|
|
||||||
type: 'error',
|
|
||||||
title: 'Error Title',
|
|
||||||
description: 'Error Description',
|
|
||||||
},
|
|
||||||
onCancel: jest.fn(),
|
|
||||||
onSubmit: () => {},
|
|
||||||
};
|
|
||||||
const wrapper = mount(<LoginModal {...props} />);
|
|
||||||
wrapper.find('button[id="login--form-cancel"]').simulate('click');
|
|
||||||
expect(props.onCancel).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setCredentials - should set username and password in state', () => {
|
|
||||||
const props = {
|
|
||||||
visibility: true,
|
|
||||||
onCancel: () => {},
|
|
||||||
onSubmit: () => {},
|
|
||||||
};
|
|
||||||
const wrapper = mount(<LoginModal {...props} />);
|
|
||||||
const { setCredentials } = wrapper.instance();
|
|
||||||
|
|
||||||
expect(setCredentials('username', eventUsername)).toBeUndefined();
|
|
||||||
expect(wrapper.state('form').username.value).toEqual('xyz');
|
|
||||||
|
|
||||||
expect(setCredentials('password', eventPassword)).toBeUndefined();
|
|
||||||
expect(wrapper.state('form').password.value).toEqual('1234');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('validateCredentials: should validate credentials', async () => {
|
|
||||||
const props = {
|
|
||||||
visibility: true,
|
|
||||||
onCancel: () => {},
|
|
||||||
onSubmit: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = mount(<LoginModal {...props} />);
|
|
||||||
const instance = wrapper.instance();
|
|
||||||
|
|
||||||
instance.submitCredentials = jest.fn();
|
|
||||||
const { handleValidateCredentials, setCredentials, submitCredentials } = instance;
|
|
||||||
|
|
||||||
expect(setCredentials('username', eventUsername)).toBeUndefined();
|
|
||||||
expect(wrapper.state('form').username.value).toEqual('xyz');
|
|
||||||
|
|
||||||
expect(setCredentials('password', eventPassword)).toBeUndefined();
|
|
||||||
expect(wrapper.state('form').password.value).toEqual('1234');
|
|
||||||
|
|
||||||
handleValidateCredentials(event);
|
|
||||||
expect(event.preventDefault).toHaveBeenCalled();
|
|
||||||
expect(wrapper.state('form').username.pristine).toEqual(false);
|
|
||||||
expect(wrapper.state('form').password.pristine).toEqual(false);
|
|
||||||
|
|
||||||
expect(submitCredentials).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('submitCredentials: should submit credentials', async () => {
|
|
||||||
const props = {
|
|
||||||
onSubmit: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = mount(<LoginModal {...props} />);
|
|
||||||
const { setCredentials, submitCredentials } = wrapper.instance();
|
|
||||||
expect(setCredentials('username', eventUsername)).toBeUndefined();
|
|
||||||
expect(wrapper.state('form').username.value).toEqual('xyz');
|
|
||||||
|
|
||||||
expect(setCredentials('password', eventPassword)).toBeUndefined();
|
|
||||||
expect(wrapper.state('form').password.value).toEqual('1234');
|
|
||||||
|
|
||||||
await submitCredentials();
|
|
||||||
expect(props.onSubmit).toHaveBeenCalledWith('xyz', '1234');
|
|
||||||
expect(wrapper.state('form').username.value).toEqual('');
|
|
||||||
expect(wrapper.state('form').username.pristine).toEqual(true);
|
|
||||||
expect(wrapper.state('form').password.value).toEqual('');
|
|
||||||
expect(wrapper.state('form').password.pristine).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,254 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import ErrorIcon from '@material-ui/icons/Error';
|
|
||||||
|
|
||||||
import Button from '../../muiComponents/Button';
|
|
||||||
import Dialog from '../../muiComponents/Dialog';
|
|
||||||
import DialogTitle from '../../muiComponents/DialogTitle';
|
|
||||||
import DialogContent from '../../muiComponents/DialogContent';
|
|
||||||
import DialogActions from '../../muiComponents/DialogActions';
|
|
||||||
import FormControl from '../../muiComponents/FormControl';
|
|
||||||
import FormHelperText from '../../muiComponents/FormHelperText';
|
|
||||||
import Input from '../../muiComponents/Input';
|
|
||||||
import InputLabel from '../../muiComponents/InputLabel';
|
|
||||||
import SnackbarContent from '../../muiComponents/SnackbarContent';
|
|
||||||
|
|
||||||
import * as classes from './styles';
|
|
||||||
|
|
||||||
interface FormFields {
|
|
||||||
required: boolean;
|
|
||||||
pristine: boolean;
|
|
||||||
helperText: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
export interface FormError {
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoginModalProps {
|
|
||||||
visibility: boolean;
|
|
||||||
error?: FormError;
|
|
||||||
onCancel: () => void;
|
|
||||||
onSubmit: (username: string, password: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoginModalState {
|
|
||||||
form: {
|
|
||||||
username: Partial<FormFields>;
|
|
||||||
password: Partial<FormFields>;
|
|
||||||
};
|
|
||||||
error?: FormError;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class LoginModal extends Component<Partial<LoginModalProps>, LoginModalState> {
|
|
||||||
constructor(props: LoginModalProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
form: {
|
|
||||||
username: {
|
|
||||||
required: true,
|
|
||||||
pristine: true,
|
|
||||||
helperText: 'Field required',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
required: true,
|
|
||||||
pristine: true,
|
|
||||||
helperText: 'Field required',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
error: props.error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
const { visibility = true, onCancel = () => null, error } = this.props as LoginModalProps;
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
data-testid={'login--form-container'}
|
|
||||||
fullWidth={true}
|
|
||||||
id={'login--form-container'}
|
|
||||||
maxWidth={'xs'}
|
|
||||||
onClose={onCancel}
|
|
||||||
open={visibility}>
|
|
||||||
<form noValidate={true} onSubmit={this.handleValidateCredentials}>
|
|
||||||
<DialogTitle>{'Login'}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
{error && this.renderLoginError(error)}
|
|
||||||
{this.renderNameField()}
|
|
||||||
{this.renderPasswordField()}
|
|
||||||
</DialogContent>
|
|
||||||
{this.renderActions()}
|
|
||||||
</form>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set login modal's username and password to current state
|
|
||||||
* Required to login
|
|
||||||
*/
|
|
||||||
public setCredentials = (name, e) => {
|
|
||||||
const { form } = this.state;
|
|
||||||
this.setState({
|
|
||||||
form: {
|
|
||||||
...form,
|
|
||||||
[name]: {
|
|
||||||
...form[name],
|
|
||||||
value: e.target.value,
|
|
||||||
pristine: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public handleUsernameChange = event => {
|
|
||||||
this.setCredentials('username', event);
|
|
||||||
};
|
|
||||||
|
|
||||||
public handlePasswordChange = event => {
|
|
||||||
this.setCredentials('password', event);
|
|
||||||
};
|
|
||||||
|
|
||||||
public handleValidateCredentials = event => {
|
|
||||||
const { form } = this.state;
|
|
||||||
// prevents default submit behavior
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
form: Object.keys(form).reduce(
|
|
||||||
(acc, key) => ({
|
|
||||||
...acc,
|
|
||||||
...{ [key]: { ...form[key], pristine: false } },
|
|
||||||
}),
|
|
||||||
{ username: {}, password: {} }
|
|
||||||
),
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (!Object.keys(form).some(id => !form[id])) {
|
|
||||||
this.submitCredentials();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
public submitCredentials = async () => {
|
|
||||||
const { form } = this.state;
|
|
||||||
const username = (form.username && form.username.value) || '';
|
|
||||||
const password = (form.password && form.password.value) || '';
|
|
||||||
const { onSubmit } = this.props;
|
|
||||||
if (onSubmit) {
|
|
||||||
await onSubmit(username, password);
|
|
||||||
}
|
|
||||||
// let's wait for API response and then set
|
|
||||||
// username and password filed to empty state
|
|
||||||
this.setState({
|
|
||||||
form: Object.keys(form).reduce(
|
|
||||||
(acc, key) => ({
|
|
||||||
...acc,
|
|
||||||
...{ [key]: { ...form[key], value: '', pristine: true } },
|
|
||||||
}),
|
|
||||||
{ username: {}, password: {} }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public renderErrorMessage(title: string, description: string): JSX.Element {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<div>
|
|
||||||
<strong>{title}</strong>
|
|
||||||
</div>
|
|
||||||
<div>{description}</div>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderMessage(title: string, description: string): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className={classes.loginErrorMsg} id={'client-snackbar'}>
|
|
||||||
<ErrorIcon className={classes.loginIcon} />
|
|
||||||
{this.renderErrorMessage(title, description)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderLoginError({ type, title, description }: FormError): JSX.Element | false {
|
|
||||||
return (
|
|
||||||
type === 'error' && (
|
|
||||||
<SnackbarContent className={classes.loginError} message={this.renderMessage(title, description)} />
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderNameField = () => {
|
|
||||||
const {
|
|
||||||
form: { username },
|
|
||||||
} = this.state;
|
|
||||||
return (
|
|
||||||
<FormControl error={!username.value && !username.pristine} fullWidth={true} required={username.required}>
|
|
||||||
<InputLabel htmlFor={'username'}>{'Username'}</InputLabel>
|
|
||||||
<Input
|
|
||||||
id={'login--form-username'}
|
|
||||||
onChange={this.handleUsernameChange}
|
|
||||||
placeholder={'Your username'}
|
|
||||||
value={username.value}
|
|
||||||
/>
|
|
||||||
{!username.value && !username.pristine && (
|
|
||||||
<FormHelperText id={'username-error'}>{username.helperText}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
public renderPasswordField = () => {
|
|
||||||
const {
|
|
||||||
form: { password },
|
|
||||||
} = this.state;
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
// className={css`
|
|
||||||
// margin-top: 8px;
|
|
||||||
// `}
|
|
||||||
error={!password.value && !password.pristine}
|
|
||||||
fullWidth={true}
|
|
||||||
required={password.required}>
|
|
||||||
<InputLabel htmlFor={'password'}>{'Password'}</InputLabel>
|
|
||||||
<Input
|
|
||||||
id={'login--form-password'}
|
|
||||||
onChange={this.handlePasswordChange}
|
|
||||||
placeholder={'Your strong password'}
|
|
||||||
type={'password'}
|
|
||||||
value={password.value}
|
|
||||||
/>
|
|
||||||
{!password.value && !password.pristine && (
|
|
||||||
<FormHelperText id={'password-error'}>{password.helperText}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
public renderActions = () => {
|
|
||||||
const {
|
|
||||||
form: { username, password },
|
|
||||||
} = this.state;
|
|
||||||
const { onCancel } = this.props;
|
|
||||||
return (
|
|
||||||
<DialogActions className={'dialog-footer'}>
|
|
||||||
<Button color={'inherit'} id={'login--form-cancel'} onClick={onCancel} type={'button'}>
|
|
||||||
{'Cancel'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color={'inherit'}
|
|
||||||
disabled={!password.value || !username.value}
|
|
||||||
id={'login--form-submit'}
|
|
||||||
type={'submit'}>
|
|
||||||
{'Login'}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<LoginModal /> should load the component in default state 1`] = `"<div role=\\"presentation\\" class=\\"MuiDialog-root\\" data-testid=\\"login--form-container\\" id=\\"login--form-container\\" style=\\"position: fixed; z-index: 1300; right: 0px; bottom: 0px; top: 0px; left: 0px;\\"><div class=\\"MuiBackdrop-root\\" aria-hidden=\\"true\\" style=\\"opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\\"></div><div tabindex=\\"0\\" data-test=\\"sentinelStart\\"></div><div class=\\"MuiDialog-container MuiDialog-scrollPaper\\" style=\\"opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\\" role=\\"none presentation\\" tabindex=\\"-1\\"><div class=\\"MuiPaper-root MuiPaper-elevation24 MuiDialog-paper MuiDialog-paperScrollPaper MuiDialog-paperWidthXs MuiDialog-paperFullWidth MuiPaper-rounded\\" role=\\"dialog\\"><form novalidate=\\"\\"><div class=\\"MuiDialogTitle-root\\"><h2 class=\\"MuiTypography-root MuiTypography-h6\\">Login</h2></div><div class=\\"MuiDialogContent-root\\"><div class=\\"MuiFormControl-root MuiFormControl-fullWidth\\"><label class=\\"MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required\\" data-shrink=\\"false\\" for=\\"username\\">Username<span class=\\"MuiFormLabel-asterisk MuiInputLabel-asterisk\\"> *</span></label><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl\\"><input aria-invalid=\\"false\\" id=\\"login--form-username\\" placeholder=\\"Your username\\" required=\\"\\" type=\\"text\\" class=\\"MuiInputBase-input MuiInput-input\\" value=\\"\\"></div></div><div class=\\"MuiFormControl-root MuiFormControl-fullWidth\\"><label class=\\"MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required\\" data-shrink=\\"false\\" for=\\"password\\">Password<span class=\\"MuiFormLabel-asterisk MuiInputLabel-asterisk\\"> *</span></label><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl\\"><input aria-invalid=\\"false\\" id=\\"login--form-password\\" placeholder=\\"Your strong password\\" required=\\"\\" type=\\"password\\" class=\\"MuiInputBase-input MuiInput-input\\" value=\\"\\"></div></div></div><div class=\\"MuiDialogActions-root dialog-footer MuiDialogActions-spacing\\"><button class=\\"MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit\\" tabindex=\\"0\\" type=\\"button\\" id=\\"login--form-cancel\\"><span class=\\"MuiButton-label\\">Cancel</span><span class=\\"MuiTouchRipple-root\\"></span></button><button class=\\"MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit Mui-disabled Mui-disabled\\" tabindex=\\"-1\\" type=\\"submit\\" disabled=\\"\\" id=\\"login--form-submit\\"><span class=\\"MuiButton-label\\">Login</span></button></div></form></div></div><div tabindex=\\"0\\" data-test=\\"sentinelEnd\\"></div></div>"`;
|
|
||||||
|
|
||||||
exports[`<LoginModal /> should load the component with props 1`] = `"<div role=\\"presentation\\" class=\\"MuiDialog-root\\" data-testid=\\"login--form-container\\" id=\\"login--form-container\\" style=\\"position: fixed; z-index: 1300; right: 0px; bottom: 0px; top: 0px; left: 0px;\\"><div class=\\"MuiBackdrop-root\\" aria-hidden=\\"true\\" style=\\"opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\\"></div><div tabindex=\\"0\\" data-test=\\"sentinelStart\\"></div><div class=\\"MuiDialog-container MuiDialog-scrollPaper\\" style=\\"opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\\" role=\\"none presentation\\" tabindex=\\"-1\\"><div class=\\"MuiPaper-root MuiPaper-elevation24 MuiDialog-paper MuiDialog-paperScrollPaper MuiDialog-paperWidthXs MuiDialog-paperFullWidth MuiPaper-rounded\\" role=\\"dialog\\"><form novalidate=\\"\\"><div class=\\"MuiDialogTitle-root\\"><h2 class=\\"MuiTypography-root MuiTypography-h6\\">Login</h2></div><div class=\\"MuiDialogContent-root\\"><div class=\\"MuiTypography-root MuiPaper-root MuiPaper-elevation6 MuiSnackbarContent-root css-xlgaf-loginError MuiTypography-body2\\" role=\\"alert\\"><div class=\\"MuiSnackbarContent-message\\"><div class=\\"css-vvv32-loginErrorMsg\\" id=\\"client-snackbar\\"><svg class=\\"MuiSvgIcon-root css-tkvt8h-loginIcon\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path d=\\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z\\"></path></svg><span><div><strong>Error Title</strong></div><div>Error Description</div></span></div></div></div><div class=\\"MuiFormControl-root MuiFormControl-fullWidth\\"><label class=\\"MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required\\" data-shrink=\\"false\\" for=\\"username\\">Username<span class=\\"MuiFormLabel-asterisk MuiInputLabel-asterisk\\"> *</span></label><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl\\"><input aria-invalid=\\"false\\" id=\\"login--form-username\\" placeholder=\\"Your username\\" required=\\"\\" type=\\"text\\" class=\\"MuiInputBase-input MuiInput-input\\" value=\\"\\"></div></div><div class=\\"MuiFormControl-root MuiFormControl-fullWidth\\"><label class=\\"MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated Mui-required Mui-required\\" data-shrink=\\"false\\" for=\\"password\\">Password<span class=\\"MuiFormLabel-asterisk MuiInputLabel-asterisk\\"> *</span></label><div class=\\"MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl\\"><input aria-invalid=\\"false\\" id=\\"login--form-password\\" placeholder=\\"Your strong password\\" required=\\"\\" type=\\"password\\" class=\\"MuiInputBase-input MuiInput-input\\" value=\\"\\"></div></div></div><div class=\\"MuiDialogActions-root dialog-footer MuiDialogActions-spacing\\"><button class=\\"MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit\\" tabindex=\\"0\\" type=\\"button\\" id=\\"login--form-cancel\\"><span class=\\"MuiButton-label\\">Cancel</span><span class=\\"MuiTouchRipple-root\\"></span></button><button class=\\"MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-colorInherit Mui-disabled Mui-disabled\\" tabindex=\\"-1\\" type=\\"submit\\" disabled=\\"\\" id=\\"login--form-submit\\"><span class=\\"MuiButton-label\\">Login</span></button></div></form></div></div><div tabindex=\\"0\\" data-test=\\"sentinelEnd\\"></div></div>"`;
|
|
@ -1 +0,0 @@
|
|||||||
export { default } from './Login';
|
|
@ -1,23 +0,0 @@
|
|||||||
import { css } from 'emotion';
|
|
||||||
|
|
||||||
import { theme } from '../../design-tokens/theme';
|
|
||||||
|
|
||||||
export const loginDialog = css({
|
|
||||||
minWidth: '300px',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const loginError = css({
|
|
||||||
backgroundColor: `${theme.palette.red} !important`,
|
|
||||||
minWidth: 'inherit !important',
|
|
||||||
marginBottom: '10px !important',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const loginErrorMsg = css({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const loginIcon = css({
|
|
||||||
opacity: 0.9,
|
|
||||||
marginRight: '8px',
|
|
||||||
});
|
|
106
src/components/LoginDialog/LoginDialog.test.tsx
Normal file
106
src/components/LoginDialog/LoginDialog.test.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { render, waitForElement, fireEvent, waitForElementToBeRemoved } from '../../utils/test-react-testing-library';
|
||||||
|
import AppContext, { AppContextProps } from '../../App/AppContext';
|
||||||
|
import api from '../../utils/api';
|
||||||
|
|
||||||
|
import LoginDialog from './LoginDialog';
|
||||||
|
|
||||||
|
const appContextValue: AppContextProps = {
|
||||||
|
scope: '',
|
||||||
|
packages: [],
|
||||||
|
setUser: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<LoginDialog /> component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render the component in default state', () => {
|
||||||
|
const props = {
|
||||||
|
onClose: jest.fn(),
|
||||||
|
};
|
||||||
|
const { container } = render(
|
||||||
|
<AppContext.Provider value={appContextValue}>
|
||||||
|
<LoginDialog onClose={props.onClose} />
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load the component with the open prop', async () => {
|
||||||
|
const props = {
|
||||||
|
open: true,
|
||||||
|
onClose: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
<AppContext.Provider value={appContextValue}>
|
||||||
|
<LoginDialog onClose={props.onClose} open={props.open} />
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginDialogHeading = await waitForElement(() => getByText('Sign in'));
|
||||||
|
expect(loginDialogHeading).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('onClose: should close the login modal', async () => {
|
||||||
|
const props = {
|
||||||
|
open: true,
|
||||||
|
onClose: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<AppContext.Provider value={appContextValue}>
|
||||||
|
<LoginDialog onClose={props.onClose} open={props.open} />
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginDialogButton = await waitForElement(() => getByTestId('close-login-dialog-button'));
|
||||||
|
expect(loginDialogButton).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(loginDialogButton, { open: false });
|
||||||
|
expect(props.onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
test.skip('setCredentials - should set username and password in state', async () => {
|
||||||
|
const props = {
|
||||||
|
open: true,
|
||||||
|
onClose: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(api, 'request').mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
username: 'xyz',
|
||||||
|
token: 'djsadaskljd',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByPlaceholderText, getByText } = render(
|
||||||
|
<AppContext.Provider value={appContextValue}>
|
||||||
|
<LoginDialog onClose={props.onClose} open={props.open} />
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: the input's value is not being updated in the DOM
|
||||||
|
const userNameInput = getByPlaceholderText('Your username');
|
||||||
|
fireEvent.focus(userNameInput);
|
||||||
|
fireEvent.change(userNameInput, { target: { value: 'xyz' } });
|
||||||
|
|
||||||
|
// TODO: the input's value is not being updated in the DOM
|
||||||
|
const passwordInput = getByPlaceholderText('Your strong password');
|
||||||
|
fireEvent.focus(passwordInput);
|
||||||
|
fireEvent.change(passwordInput, { target: { value: '1234' } });
|
||||||
|
|
||||||
|
// TODO: submitting form does not work
|
||||||
|
const signInButton = getByText('Sign in');
|
||||||
|
fireEvent.click(signInButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.todo('validateCredentials: should validate credentials');
|
||||||
|
|
||||||
|
test.todo('submitCredentials: should submit credentials');
|
||||||
|
});
|
56
src/components/LoginDialog/LoginDialog.tsx
Normal file
56
src/components/LoginDialog/LoginDialog.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React, { useState, useContext, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { makeLogin } from '../../utils/login';
|
||||||
|
import storage from '../../utils/storage';
|
||||||
|
import Dialog from '../../muiComponents/Dialog';
|
||||||
|
import DialogContent from '../../muiComponents/DialogContent';
|
||||||
|
import AppContext from '../../App/AppContext';
|
||||||
|
|
||||||
|
import LoginDialogCloseButton from './LoginDialogCloseButton';
|
||||||
|
import LoginDialogForm, { FormValues } from './LoginDialogForm';
|
||||||
|
import LoginDialogHeader from './LoginDialogHeader';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginDialog: React.FC<Props> = ({ onClose, open = false }) => {
|
||||||
|
const appContext = useContext(AppContext);
|
||||||
|
|
||||||
|
if (!appContext) {
|
||||||
|
throw Error('The app Context was not correct used');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [error, setError] = useState();
|
||||||
|
|
||||||
|
const handleDoLogin = useCallback(
|
||||||
|
async (data: FormValues) => {
|
||||||
|
const { username, token, error } = await makeLogin(data.username, data.password);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username && token) {
|
||||||
|
storage.setItem('username', username);
|
||||||
|
storage.setItem('token', token);
|
||||||
|
appContext.setUser({ username });
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[appContext, onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog fullWidth={true} id="login--dialog" maxWidth="sm" onClose={onClose} open={open}>
|
||||||
|
<LoginDialogCloseButton onClose={onClose} />
|
||||||
|
<DialogContent>
|
||||||
|
<LoginDialogHeader />
|
||||||
|
<LoginDialogForm error={error} onSubmit={handleDoLogin} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginDialog;
|
28
src/components/LoginDialog/LoginDialogCloseButton.tsx
Normal file
28
src/components/LoginDialog/LoginDialogCloseButton.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
|
||||||
|
import DialogTitle from '../../muiComponents/DialogTitle';
|
||||||
|
import IconButton from '../../muiComponents/IconButton';
|
||||||
|
import { Theme } from '../../design-tokens/theme';
|
||||||
|
|
||||||
|
const StyledIconButton = styled(IconButton)<{ theme?: Theme }>(({ theme }) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
right: theme.spacing() / 2,
|
||||||
|
top: theme.spacing() / 2,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginDialogCloseButton: React.FC<Props> = ({ onClose }) => (
|
||||||
|
<DialogTitle>
|
||||||
|
<StyledIconButton data-testid="close-login-dialog-button" onClick={onClose}>
|
||||||
|
<CloseIcon titleAccess="Close Dialog" />
|
||||||
|
</StyledIconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LoginDialogCloseButton;
|
94
src/components/LoginDialog/LoginDialogForm.tsx
Normal file
94
src/components/LoginDialog/LoginDialogForm.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import useForm from 'react-hook-form/dist/react-hook-form.ie11';
|
||||||
|
|
||||||
|
import TextField from '../../muiComponents/TextField';
|
||||||
|
import Button from '../../muiComponents/Button';
|
||||||
|
import { Theme } from '../../design-tokens/theme';
|
||||||
|
import { LoginError } from '../../utils/login';
|
||||||
|
|
||||||
|
import LoginDialogFormError from './LoginDialogFormError';
|
||||||
|
|
||||||
|
const StyledForm = styled('form')<{ theme?: Theme }>(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)<{ theme?: Theme }>(({ theme }) => ({
|
||||||
|
margin: theme.spacing(3, 0, 2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface FormValues {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSubmit: (formValues: FormValues) => void;
|
||||||
|
error?: LoginError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginDialogForm = memo(({ onSubmit, error }: Props) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
errors,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isValid },
|
||||||
|
} = useForm<FormValues>({ mode: 'onChange' });
|
||||||
|
|
||||||
|
const onSubmitForm = (formValues: FormValues) => {
|
||||||
|
onSubmit(formValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledForm noValidate={true} onSubmit={handleSubmit(onSubmitForm)}>
|
||||||
|
<TextField
|
||||||
|
autoComplete="username"
|
||||||
|
error={!!errors.username}
|
||||||
|
fullWidth={true}
|
||||||
|
helperText={errors.username?.message}
|
||||||
|
id="login--dialog-username"
|
||||||
|
inputRef={register({
|
||||||
|
required: { value: true, message: 'This field is required' },
|
||||||
|
minLength: { value: 2, message: 'This field required the min length of 2' },
|
||||||
|
})}
|
||||||
|
label="Username"
|
||||||
|
margin="normal"
|
||||||
|
name="username"
|
||||||
|
placeholder="Your username"
|
||||||
|
required={true}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
autoComplete="current-password"
|
||||||
|
error={!!errors.password}
|
||||||
|
fullWidth={true}
|
||||||
|
helperText={errors.password?.message}
|
||||||
|
id="login--dialog-password"
|
||||||
|
inputRef={register({
|
||||||
|
required: { value: true, message: 'This field is required' },
|
||||||
|
minLength: { value: 2, message: 'This field required the min length of 2' },
|
||||||
|
})}
|
||||||
|
label="Password"
|
||||||
|
margin="normal"
|
||||||
|
name="password"
|
||||||
|
placeholder="Your strong password"
|
||||||
|
required={true}
|
||||||
|
type="password"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
{error && <LoginDialogFormError error={error} />}
|
||||||
|
<StyledButton
|
||||||
|
color="primary"
|
||||||
|
disabled={!isValid}
|
||||||
|
fullWidth={true}
|
||||||
|
id="login--dialog-button-submit"
|
||||||
|
size="large"
|
||||||
|
type="submit"
|
||||||
|
variant="contained">
|
||||||
|
{'Sign In'}
|
||||||
|
</StyledButton>
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LoginDialogForm;
|
42
src/components/LoginDialog/LoginDialogFormError.tsx
Normal file
42
src/components/LoginDialog/LoginDialogFormError.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import Error from '@material-ui/icons/Error';
|
||||||
|
|
||||||
|
import SnackbarContent from '../../muiComponents/SnackbarContent';
|
||||||
|
import Box from '../../muiComponents/Box';
|
||||||
|
import { Theme } from '../../design-tokens/theme';
|
||||||
|
import { LoginError } from '../../utils/login';
|
||||||
|
|
||||||
|
const StyledSnackbarContent = styled(SnackbarContent)<{ theme?: Theme }>(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.error.dark,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledErrorIcon = styled(Error)<{ theme?: Theme }>(({ theme }) => ({
|
||||||
|
fontSize: 20,
|
||||||
|
opacity: 0.9,
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface FormValues {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
error: LoginError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginDialogFormError = memo(({ error }: Props) => {
|
||||||
|
return (
|
||||||
|
<StyledSnackbarContent
|
||||||
|
message={
|
||||||
|
<Box alignItems="center" display="flex">
|
||||||
|
<StyledErrorIcon />
|
||||||
|
{error.description}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LoginDialogFormError;
|
44
src/components/LoginDialog/LoginDialogHeader.tsx
Normal file
44
src/components/LoginDialog/LoginDialogHeader.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import LockOutlined from '@material-ui/icons/LockOutlined';
|
||||||
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
|
||||||
|
import Heading from '../../muiComponents/Heading';
|
||||||
|
import Avatar from '../../muiComponents/Avatar';
|
||||||
|
import Box from '../../muiComponents/Box';
|
||||||
|
import IconButton from '../../muiComponents/IconButton';
|
||||||
|
import { Theme } from '../../design-tokens/theme';
|
||||||
|
|
||||||
|
const StyledAvatar = styled(Avatar)<{ theme?: Theme }>(({ theme }) => ({
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledIconButton = styled(IconButton)<{ theme?: Theme }>(({ theme }) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
right: theme.spacing() / 2,
|
||||||
|
top: theme.spacing() / 2,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginDialogHeader: React.FC<Props> = ({ onClose }) => {
|
||||||
|
return (
|
||||||
|
<Box alignItems="center" display="flex" flexDirection="column" position="relative">
|
||||||
|
{onClose && (
|
||||||
|
<StyledIconButton aria-label="Close" onClick={onClose}>
|
||||||
|
<CloseIcon />
|
||||||
|
</StyledIconButton>
|
||||||
|
)}
|
||||||
|
<StyledAvatar>
|
||||||
|
<LockOutlined />
|
||||||
|
</StyledAvatar>
|
||||||
|
<Heading>{'Sign in'}</Heading>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginDialogHeader;
|
@ -0,0 +1,3 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<LoginDialog /> component should render the component in default state 1`] = `null`;
|
1
src/components/LoginDialog/index.ts
Normal file
1
src/components/LoginDialog/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './LoginDialog';
|
@ -114,7 +114,6 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
<a href={homepage} target={'_blank'}>
|
<a href={homepage} target={'_blank'}>
|
||||||
<Tooltip aria-label={'Homepage'} title={'Visit homepage'}>
|
<Tooltip aria-label={'Homepage'} title={'Visit homepage'}>
|
||||||
<IconButton aria-label={'Homepage'}>
|
<IconButton aria-label={'Homepage'}>
|
||||||
{/* eslint-disable-next-line react/jsx-max-depth */}
|
|
||||||
<HomeIcon />
|
<HomeIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -128,7 +127,6 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
<a href={bugs.url} target={'_blank'}>
|
<a href={bugs.url} target={'_blank'}>
|
||||||
<Tooltip aria-label={'Bugs'} title={'Open an issue'}>
|
<Tooltip aria-label={'Bugs'} title={'Open an issue'}>
|
||||||
<IconButton aria-label={'Bugs'}>
|
<IconButton aria-label={'Bugs'}>
|
||||||
{/* eslint-disable-next-line react/jsx-max-depth */}
|
|
||||||
<BugReport />
|
<BugReport />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -143,7 +141,6 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
<a onClick={downloadTarball(dist.tarball.replace(`https://registry.npmjs.org/`, window.location.href))} target={'_blank'}>
|
<a onClick={downloadTarball(dist.tarball.replace(`https://registry.npmjs.org/`, window.location.href))} target={'_blank'}>
|
||||||
<Tooltip aria-label={'Download the tar file'} title={'Download tarball'}>
|
<Tooltip aria-label={'Download the tar file'} title={'Download tarball'}>
|
||||||
<IconButton aria-label={'Download'}>
|
<IconButton aria-label={'Download'}>
|
||||||
{/* eslint-disable-next-line react/jsx-max-depth */}
|
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -155,7 +152,6 @@ const Package: React.FC<PackageInterface> = ({
|
|||||||
<Grid container={true} item={true} xs={12}>
|
<Grid container={true} item={true} xs={12}>
|
||||||
<Grid item={true} xs={true}>
|
<Grid item={true} xs={true}>
|
||||||
<WrapperLink to={`/-/web/detail/${packageName}`}>
|
<WrapperLink to={`/-/web/detail/${packageName}`}>
|
||||||
{/* eslint-disable-next-line react/jsx-max-depth */}
|
|
||||||
<PackageTitle className="package-title">{packageName}</PackageTitle>
|
<PackageTitle className="package-title">{packageName}</PackageTitle>
|
||||||
</WrapperLink>
|
</WrapperLink>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
/* eslint react/jsx-max-depth: 0 */
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
@ -37,7 +35,6 @@ const RepositoryListItemText = styled(ListItemText)({
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable react/jsx-wrap-multilines */
|
|
||||||
const Repository: React.FC = () => {
|
const Repository: React.FC = () => {
|
||||||
const detailContext = React.useContext(DetailContext);
|
const detailContext = React.useContext(DetailContext);
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ const detailContextValue = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('test Version page', () => {
|
describe('test Version page', () => {
|
||||||
/* eslint-disable react/jsx-max-depth */
|
|
||||||
test('should render the version page', async () => {
|
test('should render the version page', async () => {
|
||||||
const { getByTestId, getByText } = render(
|
const { getByTestId, getByText } = render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
|
@ -59,7 +59,6 @@ describe('makeLogin', (): void => {
|
|||||||
const result = {
|
const result = {
|
||||||
error: {
|
error: {
|
||||||
description: "Username or password can't be empty!",
|
description: "Username or password can't be empty!",
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error',
|
type: 'error',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -77,8 +76,7 @@ describe('makeLogin', (): void => {
|
|||||||
test('makeLogin - login should failed with 401', async () => {
|
test('makeLogin - login should failed with 401', async () => {
|
||||||
const result = {
|
const result = {
|
||||||
error: {
|
error: {
|
||||||
description: 'bad username/password, access denied',
|
description: 'Unable to sign in',
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error',
|
type: 'error',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -91,7 +89,6 @@ describe('makeLogin', (): void => {
|
|||||||
test('makeLogin - login should failed with when no data is sent', async () => {
|
test('makeLogin - login should failed with when no data is sent', async () => {
|
||||||
const result = {
|
const result = {
|
||||||
error: {
|
error: {
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error',
|
type: 'error',
|
||||||
description: "Username or password can't be empty!",
|
description: "Username or password can't be empty!",
|
||||||
},
|
},
|
||||||
|
@ -47,7 +47,6 @@ export interface LoginBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginError {
|
export interface LoginError {
|
||||||
title: string;
|
|
||||||
type: string;
|
type: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
@ -56,7 +55,6 @@ export async function makeLogin(username?: string, password?: string): Promise<L
|
|||||||
// checks isEmpty
|
// checks isEmpty
|
||||||
if (isEmpty(username) || isEmpty(password)) {
|
if (isEmpty(username) || isEmpty(password)) {
|
||||||
const error = {
|
const error = {
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error',
|
type: 'error',
|
||||||
description: "Username or password can't be empty!",
|
description: "Username or password can't be empty!",
|
||||||
};
|
};
|
||||||
@ -77,10 +75,10 @@ export async function makeLogin(username?: string, password?: string): Promise<L
|
|||||||
};
|
};
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('login error', e.message);
|
||||||
const error = {
|
const error = {
|
||||||
title: 'Unable to login',
|
|
||||||
type: 'error',
|
type: 'error',
|
||||||
description: e.error,
|
description: 'Unable to sign in',
|
||||||
};
|
};
|
||||||
return { error };
|
return { error };
|
||||||
}
|
}
|
||||||
|
@ -30,16 +30,16 @@ describe('/ (Verdaccio Page)', () => {
|
|||||||
await clickElement('button[data-testid="header--button-login"]');
|
await clickElement('button[data-testid="header--button-login"]');
|
||||||
await page.waitFor(500);
|
await page.waitFor(500);
|
||||||
// we fill the sign in form
|
// we fill the sign in form
|
||||||
const signInDialog = await page.$('#login--form-container');
|
const signInDialog = await page.$('#login--dialog');
|
||||||
const userInput = await signInDialog.$('#login--form-username');
|
const userInput = await signInDialog.$('#login--dialog-username');
|
||||||
expect(userInput).not.toBeNull();
|
expect(userInput).not.toBeNull();
|
||||||
const passInput = await signInDialog.$('#login--form-password');
|
const passInput = await signInDialog.$('#login--dialog-password');
|
||||||
expect(passInput).not.toBeNull();
|
expect(passInput).not.toBeNull();
|
||||||
await userInput.type('test', { delay: 100 });
|
await userInput.type('test', { delay: 100 });
|
||||||
await passInput.type('test', { delay: 100 });
|
await passInput.type('test', { delay: 100 });
|
||||||
await passInput.dispose();
|
await passInput.dispose();
|
||||||
// click on log in
|
// click on log in
|
||||||
const loginButton = await page.$('#login--form-submit');
|
const loginButton = await page.$('#login--dialog-button-submit');
|
||||||
expect(loginButton).toBeDefined();
|
expect(loginButton).toBeDefined();
|
||||||
await loginButton.focus();
|
await loginButton.focus();
|
||||||
await loginButton.click({ delay: 100 });
|
await loginButton.click({ delay: 100 });
|
||||||
@ -89,8 +89,7 @@ describe('/ (Verdaccio Page)', () => {
|
|||||||
const signInButton = await page.$('button[data-testid="header--button-login"]');
|
const signInButton = await page.$('button[data-testid="header--button-login"]');
|
||||||
await signInButton.click();
|
await signInButton.click();
|
||||||
await page.waitFor(1000);
|
await page.waitFor(1000);
|
||||||
const signInDialog = await page.$('#login--form-container');
|
const signInDialog = await page.$('#login--dialog');
|
||||||
|
|
||||||
expect(signInDialog).not.toBeNull();
|
expect(signInDialog).not.toBeNull();
|
||||||
});
|
});
|
||||||
//
|
//
|
||||||
|
22
yarn.lock
22
yarn.lock
@ -3939,7 +3939,7 @@ commander@3.0.2:
|
|||||||
resolved "https://registry.verdaccio.org/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
|
resolved "https://registry.verdaccio.org/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
|
||||||
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
|
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
|
||||||
|
|
||||||
commander@^2.11.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1, commander@^2.9.0, commander@~2.20.0:
|
commander@^2.11.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1, commander@^2.9.0, commander@~2.20.0, commander@~2.20.3:
|
||||||
version "2.20.3"
|
version "2.20.3"
|
||||||
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||||
@ -9671,6 +9671,11 @@ multicast-dns@^6.0.1:
|
|||||||
dns-packet "^1.3.1"
|
dns-packet "^1.3.1"
|
||||||
thunky "^1.0.2"
|
thunky "^1.0.2"
|
||||||
|
|
||||||
|
mutationobserver-shim@0.3.3:
|
||||||
|
version "0.3.3"
|
||||||
|
resolved "https://registry.verdaccio.org/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#65869630bc89d7bf8c9cd9cb82188cd955aacd2b"
|
||||||
|
integrity sha512-gciOLNN8Vsf7YzcqRjKzlAJ6y7e+B86u7i3KXes0xfxx/nfLmozlW1Vn+Sc9x3tPIePFgc1AeIFhtRgkqTjzDQ==
|
||||||
|
|
||||||
mute-stream@0.0.7:
|
mute-stream@0.0.7:
|
||||||
version "0.0.7"
|
version "0.0.7"
|
||||||
resolved "https://registry.verdaccio.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
resolved "https://registry.verdaccio.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||||
@ -11526,6 +11531,11 @@ react-dom@16.12.0:
|
|||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.18.0"
|
scheduler "^0.18.0"
|
||||||
|
|
||||||
|
react-hook-form@3.28.12:
|
||||||
|
version "3.28.12"
|
||||||
|
resolved "https://registry.verdaccio.org/react-hook-form/-/react-hook-form-3.28.12.tgz#387173543ab8e89b47ed076ba7ad9886848d94f4"
|
||||||
|
integrity sha512-5bHttMAehOgUHeJhbCVzXguRljFrl2ZK+ydLBqsfLCOtS4++p34vp4/Hkqle5waGojZpNbDSaGUEee24WwfGQg==
|
||||||
|
|
||||||
react-hot-loader@4.12.18:
|
react-hot-loader@4.12.18:
|
||||||
version "4.12.18"
|
version "4.12.18"
|
||||||
resolved "https://registry.verdaccio.org/react-hot-loader/-/react-hot-loader-4.12.18.tgz#a9029e34af2690d76208f9a35189d73c2dfea6a7"
|
resolved "https://registry.verdaccio.org/react-hot-loader/-/react-hot-loader-4.12.18.tgz#a9029e34af2690d76208f9a35189d73c2dfea6a7"
|
||||||
@ -13687,7 +13697,7 @@ uglify-js@3.4.x:
|
|||||||
commander "~2.19.0"
|
commander "~2.19.0"
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
uglify-js@^3.1.4, uglify-js@^3.6.0:
|
uglify-js@^3.1.4:
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.verdaccio.org/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5"
|
resolved "https://registry.verdaccio.org/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5"
|
||||||
integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==
|
integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==
|
||||||
@ -13695,6 +13705,14 @@ uglify-js@^3.1.4, uglify-js@^3.6.0:
|
|||||||
commander "~2.20.0"
|
commander "~2.20.0"
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
|
uglify-js@^3.6.0:
|
||||||
|
version "3.7.1"
|
||||||
|
resolved "https://registry.verdaccio.org/uglify-js/-/uglify-js-3.7.1.tgz#35c7de17971a4aa7689cd2eae0a5b39bb838c0c5"
|
||||||
|
integrity sha512-pnOF7jY82wdIhATVn87uUY/FHU+MDUdPLkmGFvGoclQmeu229eTkbG5gjGGBi3R7UuYYSEeYXY/TTY5j2aym2g==
|
||||||
|
dependencies:
|
||||||
|
commander "~2.20.3"
|
||||||
|
source-map "~0.6.1"
|
||||||
|
|
||||||
uglifyjs-webpack-plugin@2.2.0:
|
uglifyjs-webpack-plugin@2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.verdaccio.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.2.0.tgz#e75bc80e7f1937f725954c9b4c5a1e967ea9d0d7"
|
resolved "https://registry.verdaccio.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.2.0.tgz#e75bc80e7f1937f725954c9b4c5a1e967ea9d0d7"
|
||||||
|
Loading…
Reference in New Issue
Block a user