Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pr/108 #254

Merged
merged 32 commits into from
Dec 4, 2017
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2309066
Open a stub search box on "Add Dependency" click
tansongyang Oct 19, 2017
9a298ea
Connect to yarn's npm index
tansongyang Oct 21, 2017
f919431
Add click handler to hits
tansongyang Oct 21, 2017
a3691f7
Control InstantSearch
tansongyang Oct 21, 2017
b24892b
Populate version dropdown on select
tansongyang Oct 21, 2017
4450565
Add dependency on confirm
tansongyang Oct 21, 2017
b4fe375
Add search box autofocus
tansongyang Oct 21, 2017
6952460
Styled left side of dependency search results
tansongyang Nov 20, 2017
3db67ca
Merge branch 'master' into pr/108
tansongyang Nov 23, 2017
c9211f6
Move pr/108 files to new locations
Nov 26, 2017
3261729
Format download count
Nov 26, 2017
c79f280
Limit hits per page
Nov 26, 2017
36a97d6
Add GitHub link
Nov 26, 2017
e9c9fbd
Add homepage link (and tooltips)
Nov 26, 2017
9918629
Add version select
Nov 26, 2017
d2b3b4b
Style dependency search box
Nov 26, 2017
f19555d
Make lines between dependencies softer
Nov 26, 2017
2363310
Include version in DependencyHit onClick
Nov 26, 2017
0f9006c
Support "Enter" click on DependencyHit
Nov 26, 2017
2df75e7
Restore add dependency functionality
Nov 26, 2017
ad4e9b3
Use codesandbox API key for Algolia search
Dec 2, 2017
a339148
Use Downshift for hit list
Dec 3, 2017
7598f0a
Refactor to remove unnecesary array
Dec 3, 2017
df686ce
Style search box
Dec 3, 2017
5452592
Focus search box
Dec 3, 2017
8dfa794
Add pagination controls
Dec 3, 2017
78cac5a
Move pagination under search bar
Dec 3, 2017
1a4732c
Add to contributors
Dec 3, 2017
3a81a8d
Merge branch 'master' into pr/108
Dec 4, 2017
66853a8
Fix lint errors
Dec 4, 2017
6e5bc79
Clean code
Dec 4, 2017
c3febdc
Re-add to contributors
Dec 4, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,90 +1,64 @@
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import styled from 'styled-components';

import Button from 'app/components/buttons/Button';
import WorkspaceInputContainer from '../WorkspaceInputContainer';
import modalActionCreators from 'app/store/modal/actions';

const ButtonContainer = styled.div`margin: 0.5rem 1rem;`;
import SearchDependencies from './SearchDependencies';

type State = {
name: string,
version: string,
};
const ButtonContainer = styled.div`margin: 0.5rem 1rem;`;

type Props = {
addDependency: (dependency: string, version: string) => Promise<boolean>,
existingDependencies: Array<string>,
modalActions: typeof modalActionCreators,
};

type State = {
processing: boolean,
};

const initialState = {
name: '',
version: '',
const initialState: State = {
processing: false,
};

export default class AddVersion extends React.PureComponent {
state = initialState;
const mapDispatchToProps = dispatch => ({
modalActions: bindActionCreators(modalActionCreators, dispatch),
});

state: State;
class AddVersion extends React.PureComponent {
props: Props;
state = initialState;

setName = (e: KeyboardEvent) => {
const { existingDependencies } = this.props;
const name = e.target.value;
this.setState({ name, replacing: existingDependencies.includes(name) });
};

setVersion = (e: KeyboardEvent) => {
this.setState({ version: e.target.value });
};

addDependency = async () => {
if (this.state.name) {
await this.props.addDependency(this.state.name, this.state.version);
this.setState(initialState);
addDependency = async (name, version) => {
if (name) {
this.props.modalActions.closeModal();
this.setState({ processing: true });
await this.props.addDependency(name, version);
this.setState({ processing: false });
}
};

handleKeyUp = (e: KeyboardEvent) => {
if (e.keyCode === 13) {
// Enter
this.addDependency();
}
openModal = () => {
this.props.modalActions.openModal({
width: 600,
Body: <SearchDependencies onConfirm={this.addDependency} />,
});
};

render() {
const { name, version, replacing } = this.state;
const { processing } = this.props;
const isValid = name !== '';
const { processing } = this.state;
return (
<div style={{ position: 'relative' }}>
<WorkspaceInputContainer>
<input
style={{ flex: 3 }}
placeholder="package name"
value={name}
onChange={this.setName}
onKeyUp={this.handleKeyUp}
/>
<input
style={{ flex: 1 }}
placeholder="version"
value={version}
onChange={this.setVersion}
onKeyUp={this.handleKeyUp}
/>
</WorkspaceInputContainer>
<ButtonContainer>
<Button
disabled={!isValid || processing}
block
small
onClick={this.addDependency}
>
{replacing ? 'Replace' : 'Add'} Package
<Button disabled={processing} block small onClick={this.openModal}>
Add Package
</Button>
</ButtonContainer>
</div>
);
}
}

export default connect(null, mapDispatchToProps)(AddVersion);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.ReactModal__Content div[class^='Modal__ModalBody'] {
/*
* app/src/app/containers/Modal.js sets ModalBody background to white.
* We don't want to risk messing up something else, so we fix that here.
*/
background: transparent;
}

.ais-SearchBox__wrapper {
margin-bottom: 0;
}

.ais-SearchBox__input {
/* theme.background */
background: #24282a;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React from 'react';
import HomeIcon from 'react-icons/lib/io/home';
import { Highlight } from 'react-instantsearch/dom';
import styled from 'styled-components';

import Tooltip from 'common/components/Tooltip';
import Select from 'app/components/Select';

import GitHubLogo from '../Git/modals/GitHubLogo';

const Container = styled.div`
display: flex;
background: ${props => props.theme.background2};
color: ${props => props.theme.white};
&:not(:last-child) {
border-bottom: 1px solid ${props => props.theme.background3};
}
`;

const Left = styled.div`
flex: 1;
`;

const Right = styled.div`
display: flex;
align-items: center;
`;

const Row = styled.div`
margin: 10px;
& > * {
margin-right: 10px;
}
`;

const Downloads = styled.span`
color: ${props => props.theme.gray};
font-size: 12px;
`;

const License = styled.span`
border: 1px solid ${props => props.theme.gray};
border-radius: 3px;
padding: 1px 3px;
color: ${props => props.theme.gray};
font-size: 12px;
`;

const IconLink = styled.a`
font-size: 1rem;
color: rgba(255, 255, 255, 0.8);
`;

type Props = {
hit: Object,
onClick: Function,
};

type State = {
selectedVersion: string,
};

const initialState: State = {
selectedVersion: '',
};

export default class DependencyHit extends React.PureComponent {
props: Props;
state = initialState;

handleClick = () => {
const { props, state } = this;
props.onClick(props.hit, state.selectedVersion);
};

handleKeyUp = e => {
const { key } = e;
if (key === 'Enter') {
this.handleClick();
}
};

handleVersionChange = e => {
const selectedVersion = e.target.value;
this.setState({ selectedVersion });
};

render() {
const { hit, onClick } = this.props;
const versions = Object.keys(hit.versions);
versions.reverse();
return (
<Container
role="button"
tabIndex={0}
onClick={this.handleClick}
onKeyUp={this.handleKeyUp}
>
<Left>
<Row>
<Highlight attributeName="name" hit={hit} />
<Downloads>{formatDownloads(hit.downloadsLast30Days)}</Downloads>
{hit.license && <License>{hit.license}</License>}
</Row>
<Row>{hit.description}</Row>
</Left>
<Right>
<Row>
{hit.githubRepo && (
<Tooltip title={`GitHub repository of ${hit.name}`}>
<IconLink
href={makeGitHubRepoUrl(hit.githubRepo)}
target="_blank"
rel="noreferrer noopener"
onClick={stopPropagation}
>
<GitHubLogo />
</IconLink>
</Tooltip>
)}
{hit.homepage && (
<Tooltip title={`Homepage of ${hit.name}`}>
<IconLink
href={hit.homepage}
target="_blank"
rel="noreferrer noopener"
onClick={stopPropagation}
>
<HomeIcon />
</IconLink>
</Tooltip>
)}
<Select
onClick={stopPropagation}
onChange={this.handleVersionChange}
value={this.state.selectedVersion}
>
{versions.map(v => <option key={v}>{v}</option>)}
</Select>
</Row>
</Right>
</Container>
);
}
}

export function hitComponent(onClick) {
return ({ hit }) => {
return <DependencyHit hit={hit} onClick={onClick} />;
};
}

export function formatDownloads(downloads) {
if (downloads >= 1000000) {
const x = Math.floor(downloads / 100000);
const millions = x / 10;
return millions + 'M';
}
if (downloads >= 1000) {
const x = Math.floor(downloads / 100);
const thousands = x / 10;
return thousands + 'K';
}
return downloads.toString();
}

function makeGitHubRepoUrl(repo) {
return `https://github.com/${repo.user}/${repo.project}`;
}

function stopPropagation(e) {
e.stopPropagation();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { formatDownloads } from './DependencyHit';

describe('formatDownloads', () => {
it('leaves numbers under 1000 unchanged', () => {
expect(formatDownloads(0)).toBe('0');
expect(formatDownloads(999)).toBe('999');
});

it('formats numbers between 1000000 and 1000 with "K"', () => {
expect(formatDownloads(1000)).toBe('1K');
expect(formatDownloads(1099)).toBe('1K');
expect(formatDownloads(1100)).toBe('1.1K');
expect(formatDownloads(999999)).toBe('999.9K');
});

it('formats numbers 1000000 and above "M"', () => {
expect(formatDownloads(1000000)).toBe('1M');
expect(formatDownloads(1100000)).toBe('1.1M');
expect(formatDownloads(999999999)).toBe('999.9M');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import {
InstantSearch,
Configure,
Hits,
SearchBox,
} from 'react-instantsearch/dom';

import 'app/pages/Search/Search.css';
import './Dependencies.css';

import { hitComponent } from './DependencyHit';

type Props = {
onConfirm: (dependency: string, version: string) => Promise<boolean>,
};

type State = {
searchState: Object,
};

const initialState: State = {
searchState: {},
};

export default class SearchDependencies extends React.PureComponent {
props: Props;
state = initialState;

handleHitClick = (hit, selectedVersion) => {
this.props.onConfirm(hit.name, selectedVersion);
};
hitComponent = hitComponent(this.handleHitClick);

handleSearchStateChange = searchState => {
this.setState({ searchState });
};

render() {
const { searchState } = this.state;
const showHits = searchState.query;
// Copied from https://github.com/yarnpkg/website/blob/956150946634b1e6ae8c3aebd3fd269744180738/scripts/sitemaps.js
// TODO: Use our own key
return (
<div>
<InstantSearch
appId="OFCNCOG2CU"
apiKey="f54e21fa3a2a0160595bb058179bfb1e"
indexName="npm-search"
searchState={searchState}
onSearchStateChange={this.handleSearchStateChange}
>
<Configure hitsPerPage={5} />
<SearchBox autoFocus />
{showHits && <Hits hitComponent={this.hitComponent} />}
</InstantSearch>
</div>
);
}
}