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

feat: migrating flow to typescript (#47)

This PR convert the code base to Typescript, the changes are the following:

- migrate code base to Typescript (3.4.x)
- enable `eslint` and `@typescript-eslint/eslint-plugin` (warnings still need to be addressed in future pull request
- update relevant dependencies for this PR (linting, etc)
- enable `bundlezise` (it was disabled for some reason)

* refactor: refactoring to typescript

* refactor: migrating to typescript

* refactor: applied feedbacks

* fix: fixed conflicts

* refactored: changed registry

* refactor: updated registry & removed unnecessary lib

* fix: fixed registry ur

* fix: fixed page load

* refactor: refactored footer wip

* refactor: converting to ts..wip

* refactor: converting to ts. wip

* refactor: converting to ts. wip

* refactor: converting to ts

* refactor: converting to ts

* fix: fixed load errors

* refactor: converted files to ts

* refactor: removed flow from tests

* fix: removed transpiled files

* refactor: added ts-ignore

* fix: fixed errors

* fix: fixed types

* fix: fixing jest import -.-

* fix: fixing lint errors

* fix: fixing lint errors

* fix: fixed lint errors

* refactor: removed unnecessary tsconfig's config

* fix: fixing errors

* fix: fixed warning

* fix: fixed test

* refactor: wip

* refactor: wip

* refactor: wip

* fix: fixing tests: wip

* wip

* wip

* fix: fixed search test

* wip

* fix: fixing lint errors

* fix: re-added stylelint

* refactor: updated stylelint script

* fix: fixed: 'styles.js'  were found.

* fix: fixed Search tests

* chore: enable eslint

eslint needs expecitely to know which file has to lint, by default is JS, in this case we need also ts,tsx files eslint . --ext .js,.ts

* chore: vcode eslint settings

* chore: restore eslint previous conf

* chore: clean jest config

* chore: fix eslint warnings

* chore: eslint errors cleared

chore: clean warnings

chore: remove github actions test phases

chore: remove dupe rule

* chore: update handler name

* chore: restore logo from img to url css prop

- loading images with css is more performant than using img html tags, switching this might be a breaking change
- restore no-empty-source seems the linting do not accept false
- update snapshots
- remove @material-ui/styles

* chore: update stylelint linting

* chore: update stylelint linting

* chore: fix a mistake on move tabs to a function

* chore: eanble bundlezie

* chore: use default_executor in circleci

* chore: update readme
This commit is contained in:
Priscila Oliveira
2019-06-20 14:37:28 +02:00
committed by Juan Picado @jotadeveloper
parent 7d1764458b
commit 6b5d0b7e2e
358 changed files with 4730 additions and 58431 deletions

94
src/App/App.test.tsx Normal file
View File

@@ -0,0 +1,94 @@
import React from 'react';
import { mount } from 'enzyme';
import storage from '../utils/storage';
import App from './App';
import { generateTokenWithTimeRange } from '../../jest/unit/components/__mocks__/token';
jest.mock('../utils/storage', () => {
class LocalStorageMock {
store: object;
public constructor() {
this.store = {};
}
public clear(): void {
this.store = {};
}
public getItem(key): unknown {
return this.store[key] || null;
}
public setItem(key, value): void {
this.store[key] = value.toString();
}
public removeItem(key): void {
delete this.store[key];
}
}
return new LocalStorageMock();
});
jest.mock('../utils/api', () => ({
request: require('../../jest/unit/components/__mocks__/api').default.request,
}));
describe('App', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<App />);
});
test('toggleLoginModal: should toggle the value in state', () => {
const { handleToggleLoginModal } = wrapper.instance();
expect(wrapper.state().showLoginModal).toBeFalsy();
handleToggleLoginModal();
expect(wrapper.state('showLoginModal')).toBeTruthy();
expect(wrapper.state('error')).toEqual({});
});
test('isUserAlreadyLoggedIn: token already available in storage', async () => {
storage.setItem('username', 'verdaccio');
storage.setItem('token', generateTokenWithTimeRange(24));
const { isUserAlreadyLoggedIn } = wrapper.instance();
isUserAlreadyLoggedIn();
expect(wrapper.state('user').username).toEqual('verdaccio');
});
test('handleLogout - logouts the user and clear localstorage', async () => {
const { handleLogout } = wrapper.instance();
storage.setItem('username', 'verdaccio');
storage.setItem('token', 'xxxx.TOKEN.xxxx');
await handleLogout();
expect(wrapper.state('user')).toEqual({});
expect(wrapper.state('isUserLoggedIn')).toBeFalsy();
});
test('handleDoLogin - login the user successfully', async () => {
const { handleDoLogin } = wrapper.instance();
await handleDoLogin('sam', '1234');
const result = {
username: 'sam',
token: 'TEST_TOKEN',
};
expect(wrapper.state('isUserLoggedIn')).toBeTruthy();
expect(wrapper.state('showLoginModal')).toBeFalsy();
expect(storage.getItem('username')).toEqual('sam');
expect(storage.getItem('token')).toEqual('TEST_TOKEN');
expect(wrapper.state('user')).toEqual(result);
});
test('handleDoLogin - authentication failure', async () => {
const { handleDoLogin } = wrapper.instance();
await handleDoLogin('sam', '12345');
const result = {
description: 'bad username/password, access denied',
title: 'Unable to login',
type: 'error',
};
expect(wrapper.state('user')).toEqual({});
expect(wrapper.state('error')).toEqual(result);
});
});

View File

@@ -1,33 +1,31 @@
/**
* @prettier
*/
import React, { Component, Fragment } from 'react';
import React, { Component, ReactElement } from 'react';
import isNil from 'lodash/isNil';
import storage from './utils/storage';
import { makeLogin, isTokenExpire } from './utils/login';
import storage from '../utils/storage';
import { makeLogin, isTokenExpire } from '../utils/login';
import Loading from './components/Loading';
import LoginModal from './components/Login';
import Header from './components/Header';
import { Container, Content } from './components/Layout';
import RouterApp from './router';
import API from './utils/api';
import './styles/typeface-roboto.scss';
import './styles/main.scss';
import Loading from '../components/Loading';
import LoginModal from '../components/Login';
import Header from '../components/Header';
import { Container, Content } from '../components/Layout';
import RouterApp from '../router';
import API from '../utils/api';
import '../styles/typeface-roboto.scss';
import '../styles/main.scss';
import 'normalize.css';
import Footer from './components/Footer';
import Footer from '../components/Footer';
export const AppContext = React.createContext();
export const AppContext = React.createContext<null>(null);
export const AppContextProvider = AppContext.Provider;
export const AppContextConsumer = AppContext.Consumer;
export default class App extends Component {
state = {
export default class App extends Component<any, any> {
public state = {
error: {},
// @ts-ignore
logoUrl: window.VERDACCIO_LOGO,
user: {},
// @ts-ignore
scope: window.VERDACCIO_SCOPE ? `${window.VERDACCIO_SCOPE}:` : '',
showLoginModal: false,
isUserLoggedIn: false,
@@ -35,37 +33,40 @@ export default class App extends Component {
isLoading: true,
};
componentDidMount() {
public componentDidMount(): void {
this.isUserAlreadyLoggedIn();
this.loadOnHandler();
}
// eslint-disable-next-line no-unused-vars
componentDidUpdate(_, prevState) {
public componentDidUpdate(_, prevState): void {
const { isUserLoggedIn } = this.state;
if (prevState.isUserLoggedIn !== isUserLoggedIn) {
this.loadOnHandler();
}
}
render() {
public render(): React.ReactElement<HTMLDivElement> {
const { isLoading, isUserLoggedIn, packages, logoUrl, user, scope } = this.state;
const context: any = { isUserLoggedIn, packages, logoUrl, user, scope };
return (
// @ts-ignore
<Container isLoading={isLoading}>
{isLoading ? (
<Loading />
) : (
<Fragment>
<AppContextProvider value={{ isUserLoggedIn, packages, logoUrl, user, scope }}>{this.renderContent()}</AppContextProvider>
</Fragment>
<>
<AppContextProvider value={context}>{this.renderContent()}</AppContextProvider>
</>
)}
{this.renderLoginModal()}
</Container>
);
}
isUserAlreadyLoggedIn = () => {
public isUserAlreadyLoggedIn = () => {
// checks for token validity
const token = storage.getItem('token');
const username = storage.getItem('username');
@@ -79,10 +80,12 @@ export default class App extends Component {
}
};
loadOnHandler = async () => {
public loadOnHandler = async () => {
try {
// @ts-ignore
this.req = await API.request('packages', 'GET');
this.setState({
// @ts-ignore
packages: this.req,
isLoading: false,
});
@@ -96,7 +99,7 @@ export default class App extends Component {
}
};
setLoading = isLoading =>
public setLoading = isLoading =>
this.setState({
isLoading,
});
@@ -105,8 +108,9 @@ export default class App extends Component {
* Toggles the login modal
* Required by: <LoginModal /> <Header />
*/
handleToggleLoginModal = () => {
public handleToggleLoginModal = () => {
this.setState(prevState => ({
// @ts-ignore
showLoginModal: !prevState.showLoginModal,
error: {},
}));
@@ -116,7 +120,8 @@ export default class App extends Component {
* handles login
* Required by: <Header />
*/
handleDoLogin = async (usernameValue, passwordValue) => {
public handleDoLogin = async (usernameValue, passwordValue) => {
// @ts-ignore
const { username, token, error } = await makeLogin(usernameValue, passwordValue);
if (username && token) {
@@ -133,7 +138,7 @@ export default class App extends Component {
}
};
setLoggedUser = (username, token) => {
public setLoggedUser = (username, token) => {
this.setState({
user: {
username,
@@ -148,7 +153,7 @@ export default class App extends Component {
* Logouts user
* Required by: <Header />
*/
handleLogout = () => {
public handleLogout = () => {
storage.removeItem('username');
storage.removeItem('token');
this.setState({
@@ -157,34 +162,31 @@ export default class App extends Component {
});
};
renderLoginModal = () => {
public renderLoginModal = (): ReactElement<HTMLElement> => {
const { error, showLoginModal } = this.state;
return (
<LoginModal
error={error}
onCancel={this.handleToggleLoginModal}
onChange={this.handleSetUsernameAndPassword}
onSubmit={this.handleDoLogin}
visibility={showLoginModal}
/>
);
return <LoginModal error={error} onCancel={this.handleToggleLoginModal} onSubmit={this.handleDoLogin} visibility={showLoginModal} />;
};
renderContent = () => {
public renderContent = (): ReactElement<HTMLElement> => {
return (
<Fragment>
<>
<Content>
<RouterApp onLogout={this.handleLogout} onToggleLoginModal={this.handleToggleLoginModal}>
{this.renderHeader()}
</RouterApp>
</Content>
<Footer />
</Fragment>
</>
);
};
renderHeader = () => {
const { logoUrl, user: { username } = {}, scope } = this.state;
public renderHeader = (): ReactElement<HTMLElement> => {
const {
logoUrl,
// @ts-ignore
user: { username },
scope,
} = this.state;
return <Header logo={logoUrl} onLogout={this.handleLogout} onToggleLoginModal={this.handleToggleLoginModal} scope={scope} username={username} />;
};

1
src/App/index.ts Normal file
View File

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

5
src/components/.eslintrc Normal file
View File

@@ -0,0 +1,5 @@
{
"rules": {
"no-invalid-this": 0
}
}

View File

@@ -1,16 +1,12 @@
/**
* @prettier
*/
import React, { Component } from 'react';
import React, { Component, ReactElement } from 'react';
import BugReportIcon from '@material-ui/icons/BugReport';
import DownloadIcon from '@material-ui/icons/CloudDownload';
import HomeIcon from '@material-ui/icons/Home';
import List from '@material-ui/core/List/index';
import Tooltip from '@material-ui/core/Tooltip/index';
import List from '@material-ui/core/List';
import Tooltip from '@material-ui/core/Tooltip';
import { DetailContextConsumer } from '../../pages/version/index';
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
import { Fab, ActionListItem } from './styles';
import { isURL } from '../../utils/url';
@@ -30,17 +26,17 @@ const ACTIONS = {
};
class ActionBar extends Component<any, any> {
render() {
public render(): ReactElement<HTMLElement> {
return (
<DetailContextConsumer>
{context => {
return this.renderActionBar(context);
return this.renderActionBar(context as VersionPageConsumerProps);
}}
</DetailContextConsumer>
);
}
renderIconsWithLink(link, component) {
private renderIconsWithLink(link: string, component: any): ReactElement<HTMLElement> {
return (
<a href={link} target={'_blank'}>
{component}
@@ -48,7 +44,8 @@ class ActionBar extends Component<any, any> {
);
}
renderActionBarListItems = packageMeta => {
private renderActionBarListItems = packageMeta => {
// @ts-ignore
const { latest: { bugs: { url: issue } = {}, homepage, dist: { tarball } = {} } = {} } = packageMeta;
const actionsMap = {
@@ -62,8 +59,9 @@ class ActionBar extends Component<any, any> {
if (link && isURL(link)) {
const fab = <Fab size={'small'}>{ACTIONS[value]['icon']}</Fab>;
component.push(
// @ts-ignore
<Tooltip key={key} title={ACTIONS[value]['title']}>
{this.renderIconsWithLink(link, fab)}
<>{this.renderIconsWithLink(link, fab)}</>
</Tooltip>
);
}
@@ -77,7 +75,7 @@ class ActionBar extends Component<any, any> {
);
};
renderActionBar = ({ packageMeta = {} }) => {
private renderActionBar = ({ packageMeta = {} }) => {
return <List>{this.renderActionBarListItems(packageMeta)}</List>;
};
}

View File

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

View File

@@ -1,9 +1,6 @@
/**
* @prettier
*/
import styled from 'react-emotion';
import { default as MuiFab } from '@material-ui/core/Fab';
import ListItem from '@material-ui/core/ListItem/index';
import ListItem from '@material-ui/core/ListItem';
import colors from '../../utils/styles/colors';

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { shallow } from 'enzyme';
import Author from './Author';
describe('<Author /> component', () => {
test('should render the component in default state', () => {
const wrapper = shallow(<Author />);
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -1,14 +1,10 @@
/**
* @prettier
*/
import React, { Component, ReactNode } from 'react';
import React, { Component } from 'react';
import Avatar from '@material-ui/core/Avatar';
import List from '@material-ui/core/List';
import ListItemText from '@material-ui/core/ListItemText';
import Avatar from '@material-ui/core/Avatar/index';
import List from '@material-ui/core/List/index';
import ListItemText from '@material-ui/core/ListItemText/index';
import { DetailContextConsumer } from '../../pages/version/index';
import { DetailContextConsumer } from '../../pages/version/Version';
import { Heading, AuthorListItem } from './styles';
import { isEmail } from '../../utils/url';
@@ -16,14 +12,14 @@ class Authors extends Component<any, any> {
render() {
return (
<DetailContextConsumer>
{context => {
return this.renderAuthor(context);
{(context: any) => {
return context && context.packageMeta && this.renderAuthor(context.packageMeta);
}}
</DetailContextConsumer>
);
}
renderLinkForMail(email, avatarComponent, packageName, version) {
renderLinkForMail(email: string, avatarComponent: ReactNode, packageName: string, version: string) {
if (!email || isEmail(email) === false) {
return avatarComponent;
}
@@ -35,7 +31,7 @@ class Authors extends Component<any, any> {
);
}
renderAuthor = ({ packageMeta }) => {
renderAuthor = packageMeta => {
const { author, name: packageName, version } = packageMeta.latest;
if (!author) {

View File

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

View File

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

View File

@@ -1,11 +1,6 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
import ListItem from '@material-ui/core/ListItem/index';
import Typography from '@material-ui/core/Typography/index';
import ListItem from '@material-ui/core/ListItem';
import Typography from '@material-ui/core/Typography';
export const Heading = styled(Typography)`
&& {

View File

@@ -1,10 +1,4 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import type { Node } from 'react';
import React, { KeyboardEvent } from 'react';
import Autosuggest from 'react-autosuggest';
import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse';
@@ -12,9 +6,27 @@ import MenuItem from '@material-ui/core/MenuItem';
import { fontWeight } from '../../utils/styles/sizes';
import { Wrapper, InputField, SuggestionContainer } from './styles';
import { IProps } from './types';
const renderInputComponent = (inputProps): Node => {
export interface Props {
suggestions: any[];
suggestionsLoading?: boolean;
suggestionsLoaded?: boolean;
suggestionsError?: boolean;
apiLoading?: boolean;
color?: string;
value?: string;
placeholder?: string;
startAdornment?: any;
disableUnderline?: boolean;
onChange?: (event: KeyboardEvent<HTMLInputElement>, { newValue, method }: { newValue: string; method: string }) => void;
onSuggestionsFetch?: ({ value: string }) => Promise<void>;
onCleanSuggestions?: () => void;
onClick?: (event: KeyboardEvent<HTMLInputElement>, { suggestionValue, method }: { suggestionValue: any[]; method: string }) => void;
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
onBlur?: (event: KeyboardEvent<HTMLInputElement>) => void;
}
const renderInputComponent = inputProps => {
const { ref, startAdornment, disableUnderline, onKeyDown, ...others } = inputProps;
return (
<InputField
@@ -34,21 +46,21 @@ const renderInputComponent = (inputProps): Node => {
const getSuggestionValue = (suggestion): string => suggestion.name;
const renderSuggestion = (suggestion, { query, isHighlighted }): Node => {
const renderSuggestion = (suggestion, { query, isHighlighted }) => {
const matches = match(suggestion.name, query);
const parts = parse(suggestion.name, matches);
return (
<MenuItem component={'div'} selected={isHighlighted}>
<MenuItem component="div" selected={isHighlighted}>
<div>
{parts.map((part, index) => {
return part.highlight ? (
<span href={suggestion.link} key={String(index)} style={{ fontWeight: fontWeight.semiBold }}>
<a href={suggestion.link} key={String(index)} style={{ fontWeight: fontWeight.semiBold }}>
{part.text}
</span>
</a>
) : (
<span href={suggestion.link} key={String(index)} style={{ fontWeight: fontWeight.light }}>
<a href={suggestion.link} key={String(index)} style={{ fontWeight: fontWeight.light }}>
{part.text}
</span>
</a>
);
})}
</div>
@@ -56,9 +68,9 @@ const renderSuggestion = (suggestion, { query, isHighlighted }): Node => {
);
};
const renderMessage = (message): Node => {
const renderMessage = message => {
return (
<MenuItem component={'div'} selected={false}>
<MenuItem component="div" selected={false}>
<div>{message}</div>
</MenuItem>
);
@@ -86,7 +98,7 @@ const AutoComplete = ({
suggestionsLoading = false,
suggestionsLoaded = false,
suggestionsError = false,
}: IProps): Node => {
}: Props) => {
const autosuggestProps = {
renderInputComponent,
suggestions,

View File

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

View File

@@ -1,16 +1,14 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import styled, { css } from 'react-emotion';
import Paper from '@material-ui/core/Paper';
import TextField from '../TextField';
import { IInputField } from './types';
export const Wrapper = styled.div`
export interface InputFieldProps {
color: string;
}
export const Wrapper = styled('div')`
&& {
width: 100%;
height: 32px;
@@ -19,10 +17,11 @@ export const Wrapper = styled.div`
}
`;
export const InputField = ({ color, ...others }: IInputField) => (
export const InputField: React.FC<InputFieldProps> = ({ color, ...others }) => (
<TextField
{...others}
classes={{
// @ts-ignore
input: css`
&& {
${color &&

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { shallow } from 'enzyme';
import CopyToClipBoard from './CopyToClipBoard';
import { CopyIcon } from './styles';
describe('<CopyToClipBoard /> component', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<CopyToClipBoard text={'copy text'} />);
});
test('render the component', () => {
expect(wrapper.html()).toMatchSnapshot();
});
test('should call the DOM APIs for copy to clipboard utility', () => {
const event = {
preventDefault: jest.fn(),
};
// @ts-ignore: Property 'getSelection' does not exist on type 'Global'.
global.getSelection = jest.fn(() => ({
removeAllRanges: () => {},
addRange: () => {},
}));
// @ts-ignore: Property 'document/getSelection' does not exist on type 'Global'.
const { document, getSelection } = global;
wrapper.find(CopyIcon).simulate('click', event);
expect(event.preventDefault).toHaveBeenCalled();
expect(document.createRange).toHaveBeenCalled();
expect(getSelection).toHaveBeenCalled();
expect(document.execCommand).toHaveBeenCalledWith('copy');
});
});

View File

@@ -0,0 +1,40 @@
import Tooltip from '@material-ui/core/Tooltip';
import FileCopy from '@material-ui/icons/FileCopy';
import React from 'react';
import { copyToClipBoardUtility } from '../../utils/cli-utils';
import { TEXT } from '../../utils/constants';
import { ClipBoardCopy, ClipBoardCopyText, CopyIcon } from './styles';
interface Props {
text: string;
children?: React.ReactNode;
}
const renderText: React.FC<any> = (text: string, children: React.ReactNode): React.ReactElement<HTMLElement> => {
if (children) {
return <ClipBoardCopyText>{children}</ClipBoardCopyText>;
}
return <ClipBoardCopyText>{text}</ClipBoardCopyText>;
};
const renderToolTipFileCopy = (text: string): React.ReactElement<HTMLElement> => (
<Tooltip disableFocusListener={true} title={TEXT.CLIPBOARD_COPY}>
<CopyIcon onClick={copyToClipBoardUtility(text)}>
<FileCopy />
</CopyIcon>
</Tooltip>
);
const CopyToClipBoard: React.FC<Props> = ({ text, children }) => {
return (
<ClipBoardCopy>
{renderText(text, children)}
{renderToolTipFileCopy(text)}
</ClipBoardCopy>
);
};
export default CopyToClipBoard;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<CopyToClipBoard /> component render the component 1`] = `"<div class=\\"css-1mta3t8 eb8w2fo0\\"><span class=\\"css-1m8aenu eb8w2fo1\\">copy text</span><button class=\\"MuiButtonBase-root-15 MuiIconButton-root-9 css-56v3u0 eb8w2fo2\\" tabindex=\\"0\\" type=\\"button\\" title=\\"Copy to Clipboard\\"><span class=\\"MuiIconButton-label-14\\"><svg class=\\"MuiSvgIcon-root-18\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path fill=\\"none\\" d=\\"M0 0h24v24H0z\\"></path><path d=\\"M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z\\"></path></svg></span></button></div>"`;

View File

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

View File

@@ -1,7 +1,7 @@
import IconButton from '@material-ui/core/IconButton';
import styled from 'react-emotion';
import IconButton from '@material-ui/core/IconButton/index';
export const ClipBoardCopy = styled.div`
export const ClipBoardCopy = styled('div')`
&& {
display: flex;
align-items: center;
@@ -9,7 +9,7 @@ export const ClipBoardCopy = styled.div`
}
`;
export const ClipBoardCopyText = styled.span`
export const ClipBoardCopyText = styled('span')`
&& {
display: inline-block;
text-overflow: ellipsis;

View File

@@ -1,13 +1,8 @@
/**
* @prettier
* @flow
*/
import React, { Component, Fragment } from 'react';
import React, { Component, Fragment, ReactElement } from 'react';
import { withRouter } from 'react-router-dom';
import CardContent from '@material-ui/core/CardContent/index';
import CardContent from '@material-ui/core/CardContent';
import { DetailContextConsumer } from '../../pages/version';
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
import { CardWrap, Heading, Tags, Tag } from './styles';
import NoItems from '../NoItems';
@@ -23,13 +18,13 @@ class DepDetail extends Component<any, any> {
};
}
render() {
public render(): ReactElement<HTMLElement> {
const { name, version } = this.state;
const tagText = `${name}@${version}`;
return <Tag className={'dep-tag'} clickable={true} component={'div'} label={tagText} onClick={this.handleOnClick} />;
}
handleOnClick = () => {
private handleOnClick = () => {
const { name } = this.state;
const { onLoading, history } = this.props;
@@ -41,18 +36,17 @@ class DepDetail extends Component<any, any> {
const WrapperDependencyDetail = withRouter(DepDetail);
class DependencyBlock extends Component<any, any> {
render() {
public render(): ReactElement<HTMLElement> {
const { dependencies, title } = this.props;
const deps = Object.entries(dependencies);
return (
// $FlowFixMe
<DetailContextConsumer>
{({ enableLoading }) => {
{({ enableLoading }: any) => {
return (
<CardWrap>
<CardContent>
<Heading variant={'subheading'}>{`${title} (${deps.length})`}</Heading>
<Heading variant="subheading">{`${title} (${deps.length})`}</Heading>
<Tags>{this.renderTags(deps, enableLoading)}</Tags>
</CardContent>
</CardWrap>
@@ -62,7 +56,7 @@ class DependencyBlock extends Component<any, any> {
);
}
renderTags = (deps: any, enableLoading: any) =>
private renderTags = (deps: any, enableLoading: any) =>
deps.map(dep => {
const [name, version] = dep;
@@ -71,26 +65,25 @@ class DependencyBlock extends Component<any, any> {
}
class Dependencies extends Component<any, any> {
state = {
public state = {
tabPosition: 0,
};
render() {
public render(): ReactElement<HTMLElement> {
return (
<DetailContextConsumer>
{packageMeta => {
return this.renderDependencies(packageMeta);
return this.renderDependencies(packageMeta as VersionPageConsumerProps);
}}
</DetailContextConsumer>
);
}
checkDependencyLength(dependency: Object = {}) {
private checkDependencyLength(dependency: Record<string, any> = {}): boolean {
return Object.keys(dependency).length > 0;
}
// $FlowFixMe
renderDependencies({ packageMeta }) {
private renderDependencies({ packageMeta }): ReactElement<HTMLElement> {
const { latest } = packageMeta;
const { dependencies, devDependencies, peerDependencies, name } = latest;
@@ -99,7 +92,8 @@ class Dependencies extends Component<any, any> {
const dependencyList = Object.keys(dependencyMap).reduce((result, value, key) => {
const selectedDepndency = dependencyMap[value];
if (selectedDepndency && this.checkDependencyLength(selectedDepndency)) {
result.push(<DependencyBlock className={'dependency-block'} dependencies={selectedDepndency} key={key} title={value} />);
// @ts-ignore
result.push(<DependencyBlock className="dependency-block" dependencies={selectedDepndency} key={key} title={value} />);
}
return result;
}, []);
@@ -107,7 +101,7 @@ class Dependencies extends Component<any, any> {
if (dependencyList.length) {
return <Fragment>{dependencyList}</Fragment>;
}
return <NoItems className={'no-dependencies'} text={`${name} has no dependencies.`} />;
return <NoItems className="no-dependencies" text={`${name} has no dependencies.`} />;
}
}

View File

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

View File

@@ -1,12 +1,7 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
import Card from '@material-ui/core/Card/index';
import Typography from '@material-ui/core/Typography/index';
import Chip from '@material-ui/core/Chip/index';
import Card from '@material-ui/core/Card';
import Typography from '@material-ui/core/Typography';
import Chip from '@material-ui/core/Chip';
export const CardWrap = styled(Card)`
&& {

View File

@@ -0,0 +1,7 @@
import { ReactNode } from 'react';
export interface Props {
children: ReactNode;
open: boolean;
onClose: () => void;
}

View File

@@ -0,0 +1,72 @@
import React, { Component, ReactElement, Fragment } from 'react';
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
import Readme from '../Readme';
import Versions from '../Versions';
import { preventXSS } from '../../utils/sec-utils';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import { Content } from './styles';
import Dependencies from '../Dependencies';
import UpLinks from '../UpLinks';
interface DetailContainerState {
tabPosition: number;
}
class DetailContainer extends Component<any, DetailContainerState> {
public state = {
tabPosition: 0,
};
public render(): ReactElement<HTMLElement> {
return (
<DetailContextConsumer>
{context => {
return this.renderTabs(context as VersionPageConsumerProps);
}}
</DetailContextConsumer>
);
}
private handleChange = (event: any, tabPosition: number) => {
event.preventDefault();
this.setState({ tabPosition });
};
private renderListTabs(tabPosition: number): React.ReactElement<HTMLElement> {
return (
<Tabs indicatorColor={'primary'} onChange={this.handleChange} textColor={'primary'} value={tabPosition} variant={'fullWidth'}>
<Tab id={'readme-tab'} label={'Readme'} />
<Tab id={'dependencies-tab'} label={'Dependencies'} />
<Tab id={'versions-tab'} label={'Versions'} />
<Tab id={'uplinks-tab'} label={'Uplinks'} />
</Tabs>
);
}
private renderTabs = ({ readMe }) => {
const { tabPosition } = this.state;
return (
<Fragment>
<Content>
{this.renderListTabs(tabPosition)}
<br />
{tabPosition === 0 && this.renderReadme(readMe)}
{tabPosition === 1 && <Dependencies />}
{tabPosition === 2 && <Versions />}
{tabPosition === 3 && <UpLinks />}
</Content>
</Fragment>
);
};
private renderReadme = (readMe: string): ReactElement<HTMLElement> => {
const encodedReadme = preventXSS(readMe);
return <Readme description={encodedReadme} />;
};
}
export default DetailContainer;

View File

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

View File

@@ -1,11 +1,6 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
export const Content = styled.div`
export const Content = styled('div')`
&& {
padding: 15px;
}

View File

@@ -0,0 +1,7 @@
import { ReactNode } from 'react';
export interface Props {
children: ReactNode;
open: boolean;
onClose: () => void;
}

View File

@@ -0,0 +1,87 @@
import React, { Component, ReactElement } from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import List from '@material-ui/core/List';
import ActionBar from '../ActionBar/ActionBar';
import Author from '../Author';
import Developers from '../Developers';
import Dist from '../Dist/Dist';
import Engine from '../Engines/Engines';
import Install from '../Install';
import Repository from '../Repository/Repository';
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
import { TitleListItem, TitleListItemText } from './styles';
class DetailSidebar extends Component {
public render(): ReactElement<HTMLElement> {
return <DetailContextConsumer>{context => this.renderSideBar(context as VersionPageConsumerProps)}</DetailContextConsumer>;
}
private renderSideBar = ({ packageName, packageMeta }): ReactElement<HTMLElement> => {
return (
<div className={'sidebar-info'}>
<Card>
<CardContent>
{this.renderTitle(packageName, packageMeta)}
{this.renderActionBar()}
{this.renderCopyCLI()}
{this.renderRepository()}
{this.renderEngine()}
{this.renderDist()}
{this.renderAuthor()}
{this.renderMaintainers()}
{this.renderContributors()}
</CardContent>
</Card>
</div>
);
};
private renderTitle = (packageName, packageMeta) => {
return (
<List className="detail-info">
<TitleListItem alignItems="flex-start">
<TitleListItemText primary={<b>{packageName}</b>} secondary={packageMeta.latest.description} />
</TitleListItem>
</List>
);
};
private renderCopyCLI = () => {
return <Install />;
};
private renderMaintainers = () => {
return <Developers type="maintainers" />;
};
private renderContributors = () => {
return <Developers type="contributors" />;
};
private renderRepository = () => {
return <Repository />;
};
private renderAuthor = () => {
return <Author />;
};
private renderEngine = () => {
return <Engine />;
};
private renderDist = () => {
return <Dist />;
};
private renderActionBar = () => {
return <ActionBar />;
};
}
export default DetailSidebar;

View File

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

View File

@@ -1,12 +1,7 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
import Avatar from '@material-ui/core/Avatar/index';
import ListItem from '@material-ui/core/ListItem/index';
import ListItemText from '@material-ui/core/ListItemText/index';
import Avatar from '@material-ui/core/Avatar';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import colors from '../../utils/styles/colors';

View File

@@ -1,14 +1,10 @@
/**
* @prettier
*/
import React, { Component } from 'react';
import Avatar from '@material-ui/core/Avatar';
import Add from '@material-ui/icons/Add';
import Tooltip from '@material-ui/core/Tooltip';
import { DetailContextConsumer } from '../../pages/version';
import { DetailContextConsumer } from '../../pages/version/Version';
import { Details, Heading, Content, Fab } from './styles';
import { isEmail } from '../../utils/url';
@@ -24,7 +20,7 @@ class Developers extends Component<Props, any> {
render() {
return (
<DetailContextConsumer>
{({ packageMeta }) => {
{({ packageMeta }: any) => {
const { type } = this.props;
const developerType = packageMeta.latest[type];
if (!developerType || developerType.length === 0) return null;
@@ -49,7 +45,7 @@ class Developers extends Component<Props, any> {
<Details key={developer.email}>{this.renderDeveloperDetails(developer, packageMeta)}</Details>
))}
{visibleDevs < developers.length && (
<Fab onClick={this.handleLoadMore} size={'small'}>
<Fab onClick={this.handleLoadMore} size="small">
<Add />
</Fab>
)}

View File

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

View File

@@ -1,7 +1,3 @@
/**
* @prettier
*/
import styled from 'react-emotion';
import Typography from '@material-ui/core/Typography';
import { default as MuiFab } from '@material-ui/core/Fab';

View File

@@ -1,12 +1,8 @@
/**
* @prettier
*/
import React, { Component } from 'react';
import List from '@material-ui/core/List/index';
import List from '@material-ui/core/List';
import { DetailContextConsumer } from '../../pages/version/index';
import { DetailContextConsumer } from '../../pages/version/Version';
import { Heading, DistListItem, DistChips } from './styles';
import fileSizeSI from '../../utils/file-size';
@@ -14,14 +10,14 @@ class Dist extends Component<any, any> {
render() {
return (
<DetailContextConsumer>
{context => {
{(context: any) => {
return this.renderDist(context);
}}
</DetailContextConsumer>
);
}
renderChips(dist, license) {
renderChips(dist: any, license: string) {
const distDict = {
'file-count': dist.fileCount,
size: dist.unpackedSize && fileSizeSI(dist.unpackedSize),
@@ -29,6 +25,7 @@ class Dist extends Component<any, any> {
};
const chipsList = Object.keys(distDict).reduce((componentList, title, key) => {
// @ts-ignore
const value = distDict[title];
if (value) {
const label = (
@@ -37,6 +34,7 @@ class Dist extends Component<any, any> {
<b>{title.split('-').join(' ')}</b>:{value}
</span>
);
// @ts-ignore is not assignable to parameter of type 'never'
componentList.push(<DistChips key={key} label={label} />);
}
return componentList;
@@ -45,11 +43,11 @@ class Dist extends Component<any, any> {
return chipsList;
}
renderDist = ({ packageMeta }) => {
renderDist = ({ packageMeta }: any) => {
const { dist = {}, license } = packageMeta.latest;
return (
<List subheader={<Heading variant={'subheading'}>{'Latest Distribution'}</Heading>}>
<List subheader={<Heading variant="subheading">{'Latest Distribution'}</Heading>}>
<DistListItem>{this.renderChips(dist, license)}</DistListItem>
</List>
);

View File

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

View File

@@ -1,12 +1,8 @@
/**
* @prettier
*/
import styled from 'react-emotion';
import { default as MuiFab } from '@material-ui/core/Fab/index';
import Chip from '@material-ui/core/Chip/index';
import ListItem from '@material-ui/core/ListItem/index';
import Typography from '@material-ui/core/Typography/index';
import { default as MuiFab } from '@material-ui/core/Fab';
import Chip from '@material-ui/core/Chip';
import ListItem from '@material-ui/core/ListItem';
import Typography from '@material-ui/core/Typography';
import colors from '../../utils/styles/colors';

View File

@@ -1,16 +1,13 @@
/**
* @prettier
*/
import React, { Component, ReactElement } from 'react';
import React, { Component } from 'react';
import Avatar from '@material-ui/core/Avatar';
import Grid from '@material-ui/core/Grid';
import List from '@material-ui/core/List';
import ListItemText from '@material-ui/core/ListItemText';
import Avatar from '@material-ui/core/Avatar/index';
import Grid from '@material-ui/core/Grid/index';
import List from '@material-ui/core/List/index';
import ListItemText from '@material-ui/core/ListItemText/index';
import { DetailContextConsumer } from '../../pages/version/index';
import { DetailContextConsumer, VersionPageConsumerProps } from '../../pages/version/Version';
import { Heading, EngineListItem } from './styles';
// @ts-ignore
import node from './img/node.png';
import npm from '../Install/img/npm.svg';
@@ -20,17 +17,17 @@ const ICONS = {
};
class Engine extends Component {
render() {
public render(): ReactElement<HTMLElement> {
return (
<DetailContextConsumer>
{context => {
return this.renderEngine(context);
return this.renderEngine(context as VersionPageConsumerProps);
}}
</DetailContextConsumer>
);
}
renderEngine = ({ packageMeta }) => {
private renderEngine = ({ packageMeta }): ReactElement<HTMLElement> | null => {
const { engines } = packageMeta.latest;
if (!engines) {
return null;
@@ -41,6 +38,7 @@ class Engine extends Component {
'NPM-version': engines.npm,
};
const accumulator: React.ReactNode[] = [];
const items = Object.keys(engineDict).reduce((markup, text, key) => {
const heading = engineDict[text];
if (heading) {
@@ -51,7 +49,7 @@ class Engine extends Component {
);
}
return markup;
}, []);
}, accumulator);
if (items.length < 1) {
return null;
@@ -60,7 +58,7 @@ class Engine extends Component {
return <Grid container={true}>{items}</Grid>;
};
renderListItems = (heading, text) => {
private renderListItems = (heading, text) => {
return (
<List subheader={<Heading variant={'subheading'}>{text.split('-').join(' ')}</Heading>}>
<EngineListItem>

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

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

View File

@@ -1,11 +1,6 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
import ListItem from '@material-ui/core/ListItem/index';
import Typography from '@material-ui/core/Typography/index';
import ListItem from '@material-ui/core/ListItem';
import Typography from '@material-ui/core/Typography';
export const Heading = styled(Typography)`
&& {

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { mount } from 'enzyme';
import Footer from './Footer';
jest.mock('../../../package.json', () => ({
version: '4.0.0-alpha.3',
}));
describe('<Footer /> component', () => {
let wrapper;
beforeEach(() => {
// @ts-ignore : Property 'VERDACCIO_VERSION' does not exist on type 'Window'
window.VERDACCIO_VERSION = 'v.1.0.0';
wrapper = mount(<Footer />);
// @ts-ignore : Property 'VERDACCIO_VERSION' does not exist on type 'Window'
delete window.VERDACCIO_VERSION;
});
test('should load the initial state of Footer component', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { Wrapper, Left, Right, Earth, Flags, Love, Flag, Logo, Inner, ToolTip } from './styles';
import { goToVerdaccioWebsite } from '../../utils/windows';
const renderTooltip = () => (
<ToolTip>
<Earth name="earth" size="md" />
<Flags>
<Flag name="spain" size="md" />
<Flag name="nicaragua" size="md" />
<Flag name="india" size="md" />
<Flag name="brazil" size="md" />
<Flag name="china" size="md" />
<Flag name="austria" size="md" />
</Flags>
</ToolTip>
);
const POWERED_LABEL = 'Powered by';
const MADEWITH_LABEL = ' Made with';
const ON_LABEL = 'on';
const HEARTH_EMOJI = '♥';
// @ts-ignore
const renderRight = (version = window.VERDACCIO_VERSION) => {
return (
<Right>
{POWERED_LABEL}
<Logo img={true} name="verdaccio" onClick={goToVerdaccioWebsite} pointer={true} size="md" />
{`/ ${version}`}
</Right>
);
};
const renderLeft = () => (
<Left>
{MADEWITH_LABEL}
<Love>{HEARTH_EMOJI}</Love>
{ON_LABEL}
{renderTooltip()}
</Left>
);
const Footer: React.FC = () => (
<Wrapper>
<Inner>
{renderLeft()}
{renderRight()}
</Inner>
</Wrapper>
);
export default Footer;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Footer /> component should load the initial state of Footer component 1`] = `"<div class=\\"css-i0nj2g ezbsl480\\"><div class=\\"css-hzfs9b ezbsl481\\"><div class=\\"css-d8nsp7 ezbsl482\\"> Made with<span class=\\"css-1so4oe0 ezbsl487\\">♥</span>on<span class=\\"css-1ie354y ezbsl484\\"><svg class=\\"ezbsl485 css-1kgp95j ek145dl0\\"><title>Earth</title><use xlink:href=\\"[object Object]#earth\\"></use></svg><span class=\\"css-8631ip ezbsl486\\"><svg class=\\"ezbsl488 css-f1ndto ek145dl0\\"><title>Spain</title><use xlink:href=\\"[object Object]#spain\\"></use></svg><svg class=\\"ezbsl488 css-f1ndto ek145dl0\\"><title>Nicaragua</title><use xlink:href=\\"[object Object]#nicaragua\\"></use></svg><svg class=\\"ezbsl488 css-f1ndto ek145dl0\\"><title>India</title><use xlink:href=\\"[object Object]#india\\"></use></svg><svg class=\\"ezbsl488 css-f1ndto ek145dl0\\"><title>Brazil</title><use xlink:href=\\"[object Object]#brazil\\"></use></svg><svg class=\\"ezbsl488 css-f1ndto ek145dl0\\"><title>China</title><use xlink:href=\\"[object Object]#china\\"></use></svg><svg class=\\"ezbsl488 css-f1ndto ek145dl0\\"><title>Austria</title><use xlink:href=\\"[object Object]#austria\\"></use></svg></span></span></div><div class=\\"css-1wbzdyy ezbsl483\\">Powered by<span class=\\"ezbsl488 css-i15wza ek145dl1\\" name=\\"verdaccio\\" title=\\"Verdaccio\\"><img alt=\\"Verdaccio\\" src=\\"[object Object]\\" class=\\"css-1ncdhax ek145dl2\\"></span>/ v.1.0.0</div></div></div>"`;

View File

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

View File

@@ -1,14 +1,9 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
import mq from '../../utils/styles/media';
import Icon from '../Icon';
import Icon from '../Icon/Icon';
import colors from '../../utils/styles/colors';
export const Wrapper = styled.div`
export const Wrapper = styled('div')`
&& {
background: ${colors.snow};
border-top: 1px solid ${colors.greyGainsboro};
@@ -18,31 +13,40 @@ export const Wrapper = styled.div`
}
`;
export const Inner = styled.div`
export const Inner = styled('div')`
&& {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
${mq.medium(css`
min-width: 400px;
max-width: 800px;
margin: auto;
justify-content: space-between;
`)};
${mq.large(css`
max-width: 1240px;
`)};
${() => {
// @ts-ignore
return mq.medium(css`
min-width: 400px;
max-width: 800px;
margin: auto;
justify-content: space-between;
`);
}};
${() => {
// @ts-ignore
return mq.large(css`
max-width: 1240px;
`);
}};
}
`;
export const Left = styled.div`
export const Left = styled('div')`
&& {
align-items: center;
display: none;
${mq.medium(css`
display: flex;
`)};
${() => {
// @ts-ignore
return mq.medium(css`
display: flex;
`);
}};
}
`;
@@ -52,7 +56,7 @@ export const Right = styled(Left)`
}
`;
export const ToolTip = styled.span`
export const ToolTip = styled('span')`
&& {
position: relative;
height: 18px;
@@ -65,7 +69,7 @@ export const Earth = styled(Icon)`
}
`;
export const Flags = styled.span`
export const Flags = styled('span')`
&& {
position: absolute;
background: ${colors.greyAthens};
@@ -92,7 +96,7 @@ export const Flags = styled.span`
}
`;
export const Love = styled.span`
export const Love = styled('span')`
&& {
color: ${colors.love};
padding: 0 5px;

View File

@@ -0,0 +1,124 @@
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { shallow } from 'enzyme';
import Header from './Header';
describe('<Header /> component with logged in state', () => {
let wrapper;
let routerWrapper;
let instance;
let props;
beforeEach(() => {
props = {
username: 'test user',
handleLogout: jest.fn(),
logo: '',
onToggleLoginModal: jest.fn(),
scope: 'test scope',
withoutSearch: true,
};
routerWrapper = shallow(
<Router>
<Header
logo={props.logo}
onLogout={props.handleLogout}
onToggleLoginModal={props.onToggleLoginModal}
scope={props.scope}
username={props.username}
withoutSearch={props.withoutSearch}
/>
</Router>
);
wrapper = routerWrapper.find(Header).dive();
instance = wrapper.instance();
});
test('should load the component in logged in state', () => {
const state = {
openInfoDialog: false,
packages: undefined,
registryUrl: 'http://localhost',
showMobileNavBar: false,
};
expect(wrapper.state()).toEqual(state);
expect(routerWrapper.html()).toMatchSnapshot();
});
test('handleLoggedInMenu: set anchorEl to html element value in state', () => {
// creates a sample menu
const div = document.createElement('div');
const text = document.createTextNode('sample menu');
div.appendChild(text);
const event = {
currentTarget: div,
};
instance.handleLoggedInMenu(event);
expect(wrapper.state('anchorEl')).toEqual(div);
});
});
describe('<Header /> component with logged out state', () => {
let wrapper;
let routerWrapper;
let instance;
let props;
beforeEach(() => {
props = {
handleLogout: jest.fn(),
onToggleLoginModal: jest.fn(),
scope: 'test scope',
logo: '',
withoutSearch: true,
};
routerWrapper = shallow(
<Router>
<Header
logo={props.logo}
onLogout={props.handleLogout}
onToggleLoginModal={props.onToggleLoginModal}
scope={props.scope}
withoutSearch={props.withoutSearch}
/>
</Router>
);
wrapper = routerWrapper.find(Header).dive();
instance = wrapper.instance();
});
test('should load the component in logged out state', () => {
const state = {
openInfoDialog: false,
packages: undefined,
registryUrl: 'http://localhost',
showMobileNavBar: false,
};
expect(wrapper.state()).toEqual(state);
expect(routerWrapper.html()).toMatchSnapshot();
});
test('handleLoggedInMenuClose: set anchorEl value to null in state', () => {
instance.handleLoggedInMenuClose();
expect(wrapper.state('anchorEl')).toBeNull();
});
test('handleOpenRegistryInfoDialog: set openInfoDialog to be truthy in state', () => {
instance.handleOpenRegistryInfoDialog();
expect(wrapper.state('openInfoDialog')).toBeTruthy();
});
test('handleCloseRegistryInfoDialog: set openInfoDialog to be falsy in state', () => {
instance.handleCloseRegistryInfoDialog();
expect(wrapper.state('openInfoDialog')).toBeFalsy();
});
test('handleToggleLogin: close/open popover menu', () => {
instance.handleToggleLogin();
expect(wrapper.state('anchorEl')).toBeNull();
expect(props.onToggleLoginModal).toHaveBeenCalled();
});
});

View File

@@ -1,43 +1,46 @@
/**
* @prettier
* @flow
*/
import React, { Component } from 'react';
import type { Node } from 'react';
import React, { SyntheticEvent, Component, Fragment, ReactElement } from 'react';
import { Link } from 'react-router-dom';
import Button from '@material-ui/core/Button/index';
import IconButton from '@material-ui/core/IconButton/index';
import MenuItem from '@material-ui/core/MenuItem/index';
import Menu from '@material-ui/core/Menu/index';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import Info from '@material-ui/icons/Info';
import Help from '@material-ui/icons/Help';
import Tooltip from '@material-ui/core/Tooltip/index';
import Tooltip from '@material-ui/core/Tooltip';
import AccountCircle from '@material-ui/icons/AccountCircle';
import { default as IconSearch } from '@material-ui/icons/Search';
import { getRegistryURL } from '../../utils/url';
import ExternalLink from '../Link';
import Logo from '../Logo';
import RegistryInfoDialog from '../RegistryInfoDialog';
import Label from '../Label';
import Search from '../Search';
import RegistryInfoContent from '../RegistryInfoContent';
import RegistryInfoDialog from '../RegistryInfoDialog/RegistryInfoDialog';
import Label from '../Label/Label';
import Search from '../Search/Search';
import RegistryInfoContent from '../RegistryInfoContent/RegistryInfoContent';
import { IProps, IState } from './types';
import type { ToolTipType } from './types';
import { Greetings, NavBar, InnerNavBar, MobileNavBar, InnerMobileNavBar, LeftSide, RightSide, IconSearchButton, SearchWrapper } from './styles';
class Header extends Component<IProps, IState> {
handleLoggedInMenu: Function;
handleLoggedInMenuClose: Function;
handleOpenRegistryInfoDialog: Function;
handleCloseRegistryInfoDialog: Function;
handleToggleLogin: Function;
renderInfoDialog: Function;
interface Props {
logo: string;
username?: string;
onLogout: () => void;
onToggleLoginModal: () => void;
scope: string;
withoutSearch?: boolean;
}
constructor(props: IProps) {
interface State {
anchorEl?: any;
openInfoDialog: boolean;
registryUrl: string;
showMobileNavBar: boolean;
}
type ToolTipType = 'search' | 'help' | 'info';
class Header extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
openInfoDialog: false,
@@ -46,10 +49,36 @@ class Header extends Component<IProps, IState> {
};
}
public render(): ReactElement<HTMLElement> {
const { showMobileNavBar } = this.state;
const { withoutSearch = false } = this.props;
return (
<div>
<NavBar position="static">
<InnerNavBar>
{this.renderLeftSide()}
{this.renderRightSide()}
</InnerNavBar>
{this.renderInfoDialog()}
</NavBar>
{showMobileNavBar && !withoutSearch && (
<MobileNavBar>
<InnerMobileNavBar>
<Search />
</InnerMobileNavBar>
<Button color="inherit" onClick={this.handleDismissMNav}>
{'Cancel'}
</Button>
</MobileNavBar>
)}
</div>
);
}
/**
* opens popover menu for logged in user.
*/
handleLoggedInMenu = (event: SyntheticEvent<HTMLElement>) => {
public handleLoggedInMenu = (event: SyntheticEvent<HTMLElement>) => {
this.setState({
anchorEl: event.currentTarget,
});
@@ -58,7 +87,7 @@ class Header extends Component<IProps, IState> {
/**
* closes popover menu for logged in user
*/
handleLoggedInMenuClose = () => {
public handleLoggedInMenuClose = () => {
this.setState({
anchorEl: null,
});
@@ -67,7 +96,7 @@ class Header extends Component<IProps, IState> {
/**
* opens registry information dialog.
*/
handleOpenRegistryInfoDialog = () => {
public handleOpenRegistryInfoDialog = () => {
this.setState({
openInfoDialog: true,
});
@@ -76,7 +105,7 @@ class Header extends Component<IProps, IState> {
/**
* closes registry information dialog.
*/
handleCloseRegistryInfoDialog = () => {
public handleCloseRegistryInfoDialog = () => {
this.setState({
openInfoDialog: false,
});
@@ -85,7 +114,7 @@ class Header extends Component<IProps, IState> {
/**
* close/open popover menu for logged in users.
*/
handleToggleLogin = () => {
public handleToggleLogin = () => {
const { onToggleLoginModal } = this.props;
this.setState(
{
@@ -95,20 +124,20 @@ class Header extends Component<IProps, IState> {
);
};
handleToggleMNav = () => {
public handleToggleMNav = () => {
const { showMobileNavBar } = this.state;
this.setState({
showMobileNavBar: !showMobileNavBar,
});
};
handleDismissMNav = () => {
public handleDismissMNav = () => {
this.setState({
showMobileNavBar: false,
});
};
renderLeftSide = (): Node => {
public renderLeftSide = () => {
const { withoutSearch = false } = this.props;
return (
<LeftSide>
@@ -124,21 +153,22 @@ class Header extends Component<IProps, IState> {
);
};
renderLogo = (): Node => {
public renderLogo = () => {
const { logo } = this.props;
if (logo !== '') {
return <img alt={'logo'} height={'40px'} src={logo} />;
if (logo) {
return <img alt="logo" height="40px" src={logo} />;
} else {
return <Logo />;
}
};
renderToolTipIcon = (title: string, type: ToolTipType) => {
public renderToolTipIcon = (title: string, type: ToolTipType) => {
let content;
switch (type) {
case 'help':
content = (
// @ts-ignore
<IconButton blank={true} color={'inherit'} component={ExternalLink} to={'https://verdaccio.org/docs/en/installation'}>
<Help />
</IconButton>
@@ -146,14 +176,14 @@ class Header extends Component<IProps, IState> {
break;
case 'info':
content = (
<IconButton color={'inherit'} id={'header--button-registryInfo'} onClick={this.handleOpenRegistryInfoDialog}>
<IconButton color="inherit" id="header--button-registryInfo" onClick={this.handleOpenRegistryInfoDialog}>
<Info />
</IconButton>
);
break;
case 'search':
content = (
<IconSearchButton color={'inherit'} onClick={this.handleToggleMNav}>
<IconSearchButton color="inherit" onClick={this.handleToggleMNav}>
<IconSearch />
</IconSearchButton>
);
@@ -166,7 +196,7 @@ class Header extends Component<IProps, IState> {
);
};
renderRightSide = (): Node => {
public renderRightSide = () => {
const { username = '', withoutSearch = false } = this.props;
return (
<RightSide>
@@ -176,7 +206,7 @@ class Header extends Component<IProps, IState> {
{username ? (
this.renderMenu()
) : (
<Button color={'inherit'} id={'header--button-login'} onClick={this.handleToggleLogin}>
<Button color="inherit" id="header--button-login" onClick={this.handleToggleLogin}>
{'Login'}
</Button>
)}
@@ -184,26 +214,26 @@ class Header extends Component<IProps, IState> {
);
};
renderGreetings = () => {
private renderGreetings = () => {
const { username = '' } = this.props;
return (
<>
<Greetings>{`Hi,`}</Greetings>
<Label capitalize={true} limit={140} text={username} weight={'bold'} />
</>
<Fragment>
<Greetings>{'Hi,'}</Greetings>
<Label capitalize={true} text={username} weight="bold" />
</Fragment>
);
};
/**
* render popover menu
*/
renderMenu = (): Node => {
private renderMenu = () => {
const { onLogout } = this.props;
const { anchorEl } = this.state;
const open = Boolean(anchorEl);
return (
<React.Fragment>
<IconButton color={'inherit'} id={'header--button-account'} onClick={this.handleLoggedInMenu}>
<>
<IconButton color="inherit" id="header--button-account" onClick={this.handleLoggedInMenu}>
<AccountCircle />
</IconButton>
<Menu
@@ -212,7 +242,7 @@ class Header extends Component<IProps, IState> {
vertical: 'top',
horizontal: 'right',
}}
id={'sidebar-menu'}
id="sidebar-menu"
onClose={this.handleLoggedInMenuClose}
open={open}
transformOrigin={{
@@ -220,15 +250,15 @@ class Header extends Component<IProps, IState> {
horizontal: 'right',
}}>
<MenuItem disabled={true}>{this.renderGreetings()}</MenuItem>
<MenuItem id={'header--button-logout'} onClick={onLogout}>
<MenuItem id="header--button-logout" onClick={onLogout}>
{'Logout'}
</MenuItem>
</Menu>
</React.Fragment>
</>
);
};
renderInfoDialog = (): Node => {
private renderInfoDialog = () => {
const { scope } = this.props;
const { openInfoDialog, registryUrl } = this.state;
return (
@@ -237,33 +267,6 @@ class Header extends Component<IProps, IState> {
</RegistryInfoDialog>
);
};
render() {
const { showMobileNavBar } = this.state;
const { withoutSearch = false } = this.props;
return (
<div>
<NavBar position={'static'}>
<InnerNavBar>
{this.renderLeftSide()}
{this.renderRightSide()}
</InnerNavBar>
{this.renderInfoDialog()}
</NavBar>
{showMobileNavBar &&
!withoutSearch && (
<MobileNavBar>
<InnerMobileNavBar>
<Search />
</InnerMobileNavBar>
<Button color={'inherit'} onClick={this.handleDismissMNav}>
{'Cancel'}
</Button>
</MobileNavBar>
)}
</div>
);
}
}
export default Header;

View File

@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Header /> component with logged in state should load the component in logged in state 1`] = `"<div><header class=\\"MuiPaper-root-10 MuiPaper-elevation4-16 MuiAppBar-root-1 MuiAppBar-positionStatic-5 MuiAppBar-colorPrimary-8 css-rfunvc e1jf5lit8\\"><div class=\\"MuiToolbar-root-37 MuiToolbar-regular-39 MuiToolbar-gutters-38 css-1pwdmmq e1jf5lit0\\"><div class=\\"MuiToolbar-root-37 MuiToolbar-regular-39 MuiToolbar-gutters-38 css-1vacr9s e1jf5lit3\\"><a style=\\"margin-right:1em\\" href=\\"/\\"><div class=\\"css-1tnu3ib em793ed0\\"></div></a></div><div class=\\"MuiToolbar-root-37 MuiToolbar-regular-39 MuiToolbar-gutters-38 css-m61s5i e1jf5lit2\\"><a href=\\"https://verdaccio.org/docs/en/installation\\" target=\\"_blank\\" class=\\"MuiButtonBase-root-55 MuiIconButton-root-49 MuiIconButton-colorInherit-50\\" tabindex=\\"0\\" role=\\"button\\" title=\\"Documentation\\"><span class=\\"MuiIconButton-label-54\\"><svg class=\\"MuiSvgIcon-root-58\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path fill=\\"none\\" d=\\"M0 0h24v24H0z\\"></path><path d=\\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z\\"></path></svg></span></a><button class=\\"MuiButtonBase-root-55 MuiIconButton-root-49 MuiIconButton-colorInherit-50\\" tabindex=\\"0\\" type=\\"button\\" id=\\"header--button-registryInfo\\" title=\\"Registry Information\\"><span class=\\"MuiIconButton-label-54\\"><svg class=\\"MuiSvgIcon-root-58\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path fill=\\"none\\" d=\\"M0 0h24v24H0z\\"></path><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-6h2v6zm0-8h-2V7h2v2z\\"></path></svg></span></button><button class=\\"MuiButtonBase-root-55 MuiIconButton-root-49 MuiIconButton-colorInherit-50\\" tabindex=\\"0\\" type=\\"button\\" id=\\"header--button-account\\"><span class=\\"MuiIconButton-label-54\\"><svg class=\\"MuiSvgIcon-root-58\\" 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 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z\\"></path><path fill=\\"none\\" d=\\"M0 0h24v24H0z\\"></path></svg></span></button></div></div></header></div>"`;
exports[`<Header /> component with logged out state should load the component in logged out state 1`] = `"<div><header class=\\"MuiPaper-root-10 MuiPaper-elevation4-16 MuiAppBar-root-1 MuiAppBar-positionStatic-5 MuiAppBar-colorPrimary-8 css-rfunvc e1jf5lit8\\"><div class=\\"MuiToolbar-root-37 MuiToolbar-regular-39 MuiToolbar-gutters-38 css-1pwdmmq e1jf5lit0\\"><div class=\\"MuiToolbar-root-37 MuiToolbar-regular-39 MuiToolbar-gutters-38 css-1vacr9s e1jf5lit3\\"><a style=\\"margin-right:1em\\" href=\\"/\\"><div class=\\"css-1tnu3ib em793ed0\\"></div></a></div><div class=\\"MuiToolbar-root-37 MuiToolbar-regular-39 MuiToolbar-gutters-38 css-m61s5i e1jf5lit2\\"><a href=\\"https://verdaccio.org/docs/en/installation\\" target=\\"_blank\\" class=\\"MuiButtonBase-root-55 MuiIconButton-root-49 MuiIconButton-colorInherit-50\\" tabindex=\\"0\\" role=\\"button\\" title=\\"Documentation\\"><span class=\\"MuiIconButton-label-54\\"><svg class=\\"MuiSvgIcon-root-58\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path fill=\\"none\\" d=\\"M0 0h24v24H0z\\"></path><path d=\\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z\\"></path></svg></span></a><button class=\\"MuiButtonBase-root-55 MuiIconButton-root-49 MuiIconButton-colorInherit-50\\" tabindex=\\"0\\" type=\\"button\\" id=\\"header--button-registryInfo\\" title=\\"Registry Information\\"><span class=\\"MuiIconButton-label-54\\"><svg class=\\"MuiSvgIcon-root-58\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path fill=\\"none\\" d=\\"M0 0h24v24H0z\\"></path><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-6h2v6zm0-8h-2V7h2v2z\\"></path></svg></span></button><button class=\\"MuiButtonBase-root-55 MuiButton-root-85 MuiButton-text-87 MuiButton-flat-90 MuiButton-colorInherit-106\\" tabindex=\\"0\\" type=\\"button\\" id=\\"header--button-login\\"><span class=\\"MuiButton-label-86\\">Login</span></button></div></div></header></div>"`;

View File

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

View File

@@ -1,12 +1,7 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
import AppBar from '@material-ui/core/AppBar/index';
import Toolbar from '@material-ui/core/Toolbar/index';
import IconButton from '@material-ui/core/IconButton/index';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import colors from '../../utils/styles/colors';
import mq from '../../utils/styles/media';
@@ -19,7 +14,7 @@ export const InnerNavBar = styled(Toolbar)`
}
`;
export const Greetings = styled.span`
export const Greetings = styled('span')`
&& {
margin: 0 5px 0 0;
}
@@ -38,7 +33,7 @@ export const LeftSide = styled(RightSide)`
}
`;
export const MobileNavBar = styled.div`
export const MobileNavBar = styled('div')`
&& {
align-items: center;
display: flex;
@@ -48,7 +43,7 @@ export const MobileNavBar = styled.div`
}
`;
export const InnerMobileNavBar = styled.div`
export const InnerMobileNavBar = styled('div')`
&& {
border-radius: 4px;
background-color: ${colors.greyLight};
@@ -65,7 +60,7 @@ export const IconSearchButton = styled(IconButton)`
}
`;
export const SearchWrapper = styled.div`
export const SearchWrapper = styled('div')`
&& {
display: none;
max-width: 393px;
@@ -79,28 +74,37 @@ export const NavBar = styled(AppBar)`
min-height: 60px;
display: flex;
justify-content: center;
${mq.medium(css`
${SearchWrapper} {
display: flex;
}
${IconSearchButton} {
display: none;
}
${MobileNavBar} {
display: none;
}
`)};
${mq.large(css`
${InnerNavBar} {
padding: 0 20px;
}
`)};
${mq.xlarge(css`
${InnerNavBar} {
max-width: 1240px;
width: 100%;
margin: 0 auto;
}
`)};
${() => {
// @ts-ignore
return mq.medium(css`
${SearchWrapper} {
display: flex;
}
${IconSearchButton} {
display: none;
}
${MobileNavBar} {
display: none;
}
`);
}};
${() => {
// @ts-ignore
return mq.large(css`
${InnerNavBar} {
padding: 0 20px;
}
`);
}};
${() => {
// @ts-ignore
return mq.xlarge(css`
${InnerNavBar} {
max-width: 1240px;
width: 100%;
margin: 0 auto;
}
`);
}};
}
`;

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import Help from './Help';
describe('<Help /> component', () => {
test('should render the component in default state', () => {
const wrapper = shallow(<Help />);
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -1,20 +1,15 @@
/**
* @prettier
* @flow
*/
import Button from '@material-ui/core/Button';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';
import React, { Fragment } from 'react';
import type { Node } from 'react';
import CardActions from '@material-ui/core/CardActions/index';
import CardContent from '@material-ui/core/CardContent/index';
import Button from '@material-ui/core/Button/index';
import Typography from '@material-ui/core/Typography/index';
import CopyToClipBoard from '../CopyToClipBoard/index';
import { getRegistryURL } from '../../utils/url';
import CopyToClipBoard from '../CopyToClipBoard';
import { CardStyled as Card, HelpTitle } from './styles';
function renderHeadingClipboardSegments(title: string, text: string): Node {
function renderHeadingClipboardSegments(title: string, text: string): React.ReactNode {
return (
<Fragment>
<Typography variant={'body2'}>{title}</Typography>
@@ -23,24 +18,24 @@ function renderHeadingClipboardSegments(title: string, text: string): Node {
);
}
const Help = (): Node => {
const Help: React.FC = () => {
const registryUrl = getRegistryURL();
return (
<Card id={'help-card'}>
<Card id="help-card">
<CardContent>
<Typography component={'h2'} gutterBottom={true} id={'help-card__title'} variant={'headline'}>
<Typography component="h2" gutterBottom={true} id="help-card__title" variant="headline">
{'No Package Published Yet.'}
</Typography>
<HelpTitle color={'textSecondary'} gutterBottom={true}>
<HelpTitle color="textSecondary" gutterBottom={true}>
{'To publish your first package just:'}
</HelpTitle>
{renderHeadingClipboardSegments('1. Login', `npm adduser --registry ${registryUrl}`)}
{renderHeadingClipboardSegments('2. Publish', `npm publish --registry ${registryUrl}`)}
<Typography variant={'body2'}>{'3. Refresh this page.'}</Typography>
<Typography variant="body2">{'3. Refresh this page.'}</Typography>
</CardContent>
<CardActions>
<Button color={'primary'} href={'https://verdaccio.org/docs/en/installation'} size={'small'} target={'_blank'}>
<Button color="primary" href="https://verdaccio.org/docs/en/installation" size="small" target="_blank">
{'Learn More'}
</Button>
</CardActions>

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Help /> component should render the component in default state 1`] = `"<div class=\\"MuiPaper-root-2 MuiPaper-elevation1-5 MuiPaper-rounded-3 MuiCard-root-1 css-ryznli e1wgaou60\\" id=\\"help-card\\"><div class=\\"MuiCardContent-root-29\\"><h2 class=\\"MuiTypography-root-30 MuiTypography-headline-35 MuiTypography-gutterBottom-57\\" id=\\"help-card__title\\">No Package Published Yet.</h2><p class=\\"MuiTypography-root-30 MuiTypography-body1-39 MuiTypography-colorTextSecondary-63 MuiTypography-gutterBottom-57 css-zg2fwz e1wgaou61\\">To publish your first package just:</p><p class=\\"MuiTypography-root-30 MuiTypography-body2-38\\">1. Login</p><div class=\\"css-1mta3t8 eb8w2fo0\\"><span class=\\"css-1m8aenu eb8w2fo1\\">npm adduser --registry http://localhost</span><button class=\\"MuiButtonBase-root-80 MuiIconButton-root-74 css-56v3u0 eb8w2fo2\\" tabindex=\\"0\\" type=\\"button\\" title=\\"Copy to Clipboard\\"><span class=\\"MuiIconButton-label-79\\"><svg class=\\"MuiSvgIcon-root-83\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path fill=\\"none\\" d=\\"M0 0h24v24H0z\\"></path><path d=\\"M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z\\"></path></svg></span></button></div><p class=\\"MuiTypography-root-30 MuiTypography-body2-38\\">2. Publish</p><div class=\\"css-1mta3t8 eb8w2fo0\\"><span class=\\"css-1m8aenu eb8w2fo1\\">npm publish --registry http://localhost</span><button class=\\"MuiButtonBase-root-80 MuiIconButton-root-74 css-56v3u0 eb8w2fo2\\" tabindex=\\"0\\" type=\\"button\\" title=\\"Copy to Clipboard\\"><span class=\\"MuiIconButton-label-79\\"><svg class=\\"MuiSvgIcon-root-83\\" focusable=\\"false\\" viewBox=\\"0 0 24 24\\" aria-hidden=\\"true\\" role=\\"presentation\\"><path fill=\\"none\\" d=\\"M0 0h24v24H0z\\"></path><path d=\\"M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z\\"></path></svg></span></button></div><p class=\\"MuiTypography-root-30 MuiTypography-body2-38\\">3. Refresh this page.</p></div><div class=\\"MuiCardActions-root-92\\"><a class=\\"MuiButtonBase-root-80 MuiButton-root-95 MuiButton-text-97 MuiButton-textPrimary-98 MuiButton-flat-100 MuiButton-flatPrimary-101 MuiButton-sizeSmall-118 MuiCardActions-action-94\\" tabindex=\\"0\\" role=\\"button\\" href=\\"https://verdaccio.org/docs/en/installation\\" target=\\"_blank\\"><span class=\\"MuiButton-label-96\\">Learn More</span></a></div></div>"`;

View File

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

View File

@@ -1,11 +1,6 @@
/**
* @prettier
* @flow
*/
import Card from '@material-ui/core/Card';
import Typography from '@material-ui/core/Typography';
import styled from 'react-emotion';
import Card from '@material-ui/core/Card/index';
import Typography from '@material-ui/core/Typography/index';
export const CardStyled = styled(Card)`
&& {

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import Icon from './Icon';
describe('<Icon /> component', () => {
const props = {
name: 'austria',
};
test('should render the component in default state', () => {
const wrapper = shallow(<Icon name={props.name} />);
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -1,14 +1,7 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import type { Node } from 'react';
import React, { MouseEvent } from 'react';
import capitalize from 'lodash/capitalize';
import { Svg, Img, ImgWrapper } from './styles';
import { IProps, IIconsMap } from './types';
import brazil from './img/brazil.svg';
import china from './img/china.svg';
@@ -25,8 +18,25 @@ import license from './img/license.svg';
import time from './img/time.svg';
import version from './img/version.svg';
export const Icons: $Shape<IIconsMap> = {
// flags
export interface IconsMap {
brazil: string;
spain: string;
china: string;
nicaragua: string;
pakistan: string;
austria: string;
india: string;
earth: string;
verdaccio: string;
license: string;
time: string;
law: string;
version: string;
filebinary: string;
[key: string]: string;
}
export const Icons: IconsMap = {
brazil,
spain,
china,
@@ -36,7 +46,6 @@ export const Icons: $Shape<IIconsMap> = {
austria,
earth,
verdaccio,
// other icons
filebinary,
law,
license,
@@ -44,13 +53,25 @@ export const Icons: $Shape<IIconsMap> = {
version,
};
const Icon = ({ className, name, size = 'sm', img = false, pointer = false, ...props }: IProps): Node => {
export interface Props {
name: keyof IconsMap;
className?: string;
onClick?: (event: MouseEvent<SVGElement | HTMLSpanElement>) => void;
size?: 'sm' | 'md';
pointer?: boolean;
img?: boolean;
modifiers?: any;
}
const Icon: React.FC<Props> = ({ className, name, size = 'sm', img = false, pointer = false, ...props }) => {
// @ts-ignore
const title = capitalize(name);
return img ? (
<ImgWrapper className={className} pointer={pointer} size={size} title={title} {...props}>
<ImgWrapper className={className} name={name} pointer={pointer} size={size} title={title} {...props}>
<Img alt={title} src={Icons[name]} />
</ImgWrapper>
) : (
// @ts-ignore
<Svg className={className} pointer={pointer} size={size} {...props}>
<title>{title}</title>
<use xlinkHref={`${Icons[name]}#${name}`} />

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Icon /> component should render the component in default state 1`] = `"<svg class=\\"css-3skwlp ek145dl0\\"><title>Austria</title><use xlink:href=\\"[object Object]#austria\\"></use></svg>"`;

View File

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 380 B

View File

Before

Width:  |  Height:  |  Size: 898 B

After

Width:  |  Height:  |  Size: 898 B

View File

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 833 B

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 265 B

After

Width:  |  Height:  |  Size: 265 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 464 B

After

Width:  |  Height:  |  Size: 464 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 977 B

After

Width:  |  Height:  |  Size: 977 B

View File

Before

Width:  |  Height:  |  Size: 851 B

After

Width:  |  Height:  |  Size: 851 B

View File

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 321 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 344 B

View File

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

View File

@@ -1,23 +1,12 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
import { IProps } from './types';
const getSize = (size: string) => {
const getSize = (size?: 'md' | 'sm') => {
switch (size) {
case 'md':
return `
width: 18px;
height: 18px;
`;
case 'lg':
return `
width: 20px;
height: 20px;
`;
default:
return `
width: 14px;
@@ -26,7 +15,7 @@ const getSize = (size: string) => {
}
};
const commonStyle = ({ size = 'sm', pointer, modifiers }: IProps) => css`
const commonStyle = ({ size = 'sm', pointer, modifiers }: any) => css`
&& {
display: inline-block;
cursor: ${pointer ? 'pointer' : 'default'};
@@ -35,19 +24,19 @@ const commonStyle = ({ size = 'sm', pointer, modifiers }: IProps) => css`
}
`;
export const Svg = styled.svg`
export const Svg = styled('svg')`
&& {
${commonStyle};
}
`;
export const ImgWrapper = styled.span`
export const ImgWrapper = styled('span')`
&& {
${commonStyle};
}
`;
export const Img = styled.img`
export const Img = styled('img')`
&& {
width: 100%;
height: auto;

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { shallow } from 'enzyme';
import Install from './Install';
describe('<Install /> component', () => {
test('should render the component in default state', () => {
const wrapper = shallow(<Install />);
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -1,33 +1,30 @@
/**
* @prettier
*/
import List from '@material-ui/core/List';
import ListItemText from '@material-ui/core/ListItemText';
import React, { Component } from 'react';
import List from '@material-ui/core/List/index';
import ListItemText from '@material-ui/core/ListItemText/index';
import { DetailContextConsumer } from '../../pages/version/index';
// @ts-ignore
import { DetailContextConsumer } from '../../pages/version/Version';
import CopyToClipBoard from '../CopyToClipBoard';
import { Heading, InstallItem, PackageMangerAvatar } from './styles';
// logos of package managers
import npm from './img/npm.svg';
import pnpm from './img/pnpm.svg';
import yarn from './img/yarn.svg';
import { Heading, InstallItem, PackageMangerAvatar } from './styles';
class Install extends Component {
render() {
public render() {
return (
<DetailContextConsumer>
{context => {
return this.renderCopyCLI(context);
{(context: any) => {
return context && context.packageName && this.renderCopyCLI(context);
}}
</DetailContextConsumer>
);
}
renderCopyCLI = ({ packageName }) => {
public renderCopyCLI = ({ packageName }: { packageName: string }) => {
return (
<>
<List subheader={<Heading variant={'subheading'}>{'Installation'}</Heading>}>{this.renderListItems(packageName)}</List>
@@ -35,7 +32,7 @@ class Install extends Component {
);
};
renderListItems = packageName => {
public renderListItems = (packageName: string) => {
return (
<>
<InstallItem>

View File

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

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

View File

@@ -1,12 +1,7 @@
/**
* @prettier
* @flow
*/
import Avatar from '@material-ui/core/Avatar';
import ListItem from '@material-ui/core/ListItem';
import Typography from '@material-ui/core/Typography';
import styled from 'react-emotion';
import Typography from '@material-ui/core/Typography/index';
import ListItem from '@material-ui/core/ListItem/index';
import Avatar from '@material-ui/core/Avatar/index';
export const Heading = styled(Typography)`
&& {

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import Label from './Label';
describe('<Label /> component', () => {
const props = {
text: 'test',
};
test('should render the component in default state', () => {
const wrapper = shallow(<Label text={props.text} />);
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,30 @@
import React from 'react';
import styled from 'react-emotion';
import { fontWeight } from '../../utils/styles/sizes';
interface Props {
text: string;
capitalize?: boolean;
weight?: string;
modifiers?: any;
}
const Wrapper = styled('div')`
font-weight: ${({ weight }) => {
// @ts-ignore
return fontWeight[weight];
}};
text-transform: ${({ capitalize }) => (capitalize ? 'capitalize' : 'none')};
${({ modifiers }: Props) => modifiers && modifiers};
`;
const Label: React.FC<Props> = ({ text = '', capitalize = false, weight = 'regular', ...props }) => {
return (
// @ts-ignore
<Wrapper capitalize={capitalize} weight={weight} {...props}>
{text}
</Wrapper>
);
};
export default Label;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Label /> component should render the component in default state 1`] = `"<div class=\\"css-1xfhjjm esyufg60\\">test</div>"`;

View File

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

View File

@@ -1,11 +1,6 @@
/**
* @prettier
* @flow
*/
import styled, { css } from 'react-emotion';
export const Content = styled.div`
export const Content = styled('div')`
&& {
background-color: #ffffff;
flex: 1;
@@ -15,14 +10,15 @@ export const Content = styled.div`
}
`;
export const Container = styled.div`
export const Container = styled('div')`
&& {
display: flex;
flex-direction: column;
min-height: 100vh;
overflow: hidden;
${({ isLoading }) =>
isLoading &&
${props =>
// @ts-ignore
props.isLoading &&
css`
${Content} {
background-color: #f5f6f8;

View File

@@ -0,0 +1 @@
export { Content, Container } from './Layout';

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import Link from './Link';
describe('<Link /> component', () => {
const props = {
to: 'https://github.com/verdaccio/ui',
};
test('should render the component in default state', () => {
const wrapper = shallow(<Link blank={true} to={props.to} />);
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,15 @@
import React, { ReactNode } from 'react';
interface Props {
children?: ReactNode;
to: string;
blank?: boolean;
}
const Link: React.FC<Props> = ({ children, to = '#', blank = false, ...props }) => (
<a href={to} target={blank ? '_blank' : '_self'} {...props}>
{children}
</a>
);
export default Link;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Link /> component should render the component in default state 1`] = `"<a href=\\"https://github.com/verdaccio/ui\\" target=\\"_blank\\"></a>"`;

View File

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

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { shallow } from 'enzyme';
import Loading from './Loading';
describe('<Loading /> component', () => {
test('should render the component in default state', () => {
const wrapper = shallow(<Loading />);
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -1,20 +1,14 @@
/**
* @prettier
* @flow
*/
import React from 'react';
import type { Node } from 'react';
import Logo from '../Logo';
import Spinner from '../Spinner';
import { Wrapper, Badge } from './styles';
const Loading = (): Node => (
const Loading: React.FC = () => (
<Wrapper>
<Badge>
<Logo md={true} />
<Logo />
</Badge>
<Spinner />
</Wrapper>

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Loading /> component should render the component in default state 1`] = `"<div class=\\"css-1221txa eimgwje0\\"><div class=\\"css-bxochs eimgwje1\\"><div class=\\"css-1tnu3ib em793ed0\\"></div></div><div class=\\"css-vqrgi e1ag4h8b0\\"><div class=\\"MuiCircularProgress-root-1 MuiCircularProgress-colorPrimary-4 MuiCircularProgress-indeterminate-3 css-15gl0ho e1ag4h8b1\\" style=\\"width:50px;height:50px\\" role=\\"progressbar\\"><svg class=\\"MuiCircularProgress-svg-6\\" viewBox=\\"22 22 44 44\\"><circle class=\\"MuiCircularProgress-circle-7 MuiCircularProgress-circleIndeterminate-9\\" cx=\\"44\\" cy=\\"44\\" r=\\"20.2\\" fill=\\"none\\" stroke-width=\\"3.6\\"></circle></svg></div></div></div>"`;

View File

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

View File

@@ -1,11 +1,6 @@
/**
* @prettier
* @flow
*/
import styled from 'react-emotion';
export const Wrapper = styled.div`
export const Wrapper = styled('div')`
&& {
transform: translate(-50%, -50%);
top: 50%;
@@ -14,7 +9,7 @@ export const Wrapper = styled.div`
}
`;
export const Badge = styled.div`
export const Badge = styled('div')`
&& {
margin: 0 0 30px 0;
border-radius: 25px;

View File

@@ -0,0 +1,128 @@
/**
* @prettier
*/
import React from 'react';
import { mount } from '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,
error: {},
onCancel: () => {},
onSubmit: () => {},
};
const wrapper = mount<LoginModal>(<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,
error: {},
onCancel: () => {},
onSubmit: jest.fn(),
};
const wrapper = mount<LoginModal>(<LoginModal {...props} />);
const instance = wrapper.instance();
instance.submitCredentials = jest.fn();
const { validateCredentials, 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');
validateCredentials(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>(<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);
});
});

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