Merge pull request #37 from desktop/list-repos

List repos
This commit is contained in:
Josh Abernathy 2016-06-08 15:53:34 -04:00 committed by GitHub
commit 9c8ffbe8a5
6 changed files with 194 additions and 130 deletions

View file

@ -1,12 +1,16 @@
import * as React from 'react'
import ThingList from './thing-list'
import ReposList from './repos-list'
import Info from './info'
import UsersStore from './users-store'
import User from './user'
import NotLoggedIn from './not-logged-in'
import API from './lib/api'
import {Repo} from './lib/api'
interface AppState {
selectedRow: number,
repos: Repo[],
loadingRepos: boolean,
user: User
}
@ -27,14 +31,43 @@ const ContentStyle = {
}
export default class App extends React.Component<AppProps, AppState> {
private api: API
public constructor(props: AppProps) {
super(props)
props.usersStore.onUsersChanged(users => {
this.setState({selectedRow: this.state.selectedRow, user: users[0]})
const user = users[0]
this.api = new API(user)
this.setState(Object.assign({}, this.state, {user}))
this.fetchRepos()
})
this.state = {selectedRow: -1, user: props.usersStore.getUsers()[0]}
const user = props.usersStore.getUsers()[0]
this.state = {
selectedRow: -1,
user,
loadingRepos: true,
repos: []
}
if (user) {
this.api = new API(user)
}
}
private async fetchRepos() {
const repos = await this.api.fetchRepos()
this.setState(Object.assign({}, this.state, {
loadingRepos: false,
repos
}))
}
public async componentWillMount() {
if (this.api) {
this.fetchRepos()
}
}
private renderTitlebar() {
@ -52,19 +85,38 @@ export default class App extends React.Component<AppProps, AppState> {
)
}
private renderApp() {
const selectedRepo = this.state.repos[this.state.selectedRow]
return (
<div style={ContentStyle}>
<ReposList selectedRow={this.state.selectedRow}
onSelectionChanged={row => this.handleSelectionChanged(row)}
user={this.state.user}
repos={this.state.repos}
loading={this.state.loadingRepos}/>
<Info selectedRepo={selectedRepo} user={this.state.user}/>
</div>
)
}
private renderNotLoggedIn() {
return (
<div style={ContentStyle}>
<NotLoggedIn/>
</div>
)
}
public render() {
return (
<div style={AppStyle}>
{this.renderTitlebar()}
<div style={ContentStyle}>
<ThingList selectedRow={this.state.selectedRow} onSelectionChanged={row => this.handleSelectionChanged(row)}/>
{this.state.user ? <Info selectedRow={this.state.selectedRow} user={this.state.user}/> : <NotLoggedIn/>}
</div>
{this.state.user ? this.renderApp() : this.renderNotLoggedIn()}
</div>
)
}
private handleSelectionChanged(row: number) {
this.setState({selectedRow: row, user: this.state.user})
this.setState(Object.assign({}, this.state, {selectedRow: row}))
}
}

View file

@ -20,7 +20,8 @@ interface AuthState {
let authState: AuthState = null
export async function requestToken(code: string): Promise<string> {
const response = await fetch(`${authState.endpoint}/login/oauth/access_token`, {
const urlBase = getOAuthURL(authState.endpoint)
const response = await fetch(`${urlBase}/login/oauth/access_token`, {
method: 'post',
headers: DefaultHeaders,
body: JSON.stringify({
@ -34,18 +35,29 @@ export async function requestToken(code: string): Promise<string> {
return json.access_token
}
function getOAuthURL(authState: AuthState): string {
return `${authState.endpoint}/login/oauth/authorize?client_id=${ClientID}&scope=repo&state=${authState.oAuthState}`
function getOAuthAuthorizationURL(authState: AuthState): string {
const urlBase = getOAuthURL(authState.endpoint)
return `${urlBase}/login/oauth/authorize?client_id=${ClientID}&scope=repo&state=${authState.oAuthState}`
}
function getOAuthURL(endpoint: string): string {
if (endpoint === getDotComEndpoint()) {
// GitHub.com is A Special Snowflake in that the API lives at a subdomain
// but OAuth lives on the parent domain.
return 'https://github.com'
} else {
return endpoint
}
}
export function getDotComEndpoint(): string {
return 'https://github.com'
return 'https://api.github.com'
}
export function askUserToAuth(endpoint: string) {
authState = {oAuthState: guid(), endpoint}
shell.openExternal(getOAuthURL(authState))
shell.openExternal(getOAuthAuthorizationURL(authState))
}
export function getKeyForUser(user: User): string {

View file

@ -1,24 +1,16 @@
import {shell} from 'electron'
import * as React from 'react'
import User from './user'
const Octokat = require('octokat')
const LOLZ = [
'http://www.reactiongifs.com/r/drkrm.gif',
'http://www.reactiongifs.com/r/wvy1.gif',
'http://www.reactiongifs.com/r/ihniwid.gif',
'http://www.reactiongifs.com/r/dTa.gif',
'http://www.reactiongifs.com/r/didit.gif'
]
import {Repo} from './lib/api'
interface InfoProps {
selectedRow: number,
selectedRepo: Repo,
user: User
}
interface InfoState {
userAvatarURL: string
}
const ContainerStyle = {
@ -27,77 +19,26 @@ const ContainerStyle = {
flex: 1
}
const ImageStyle = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
flex: 1
}
const AvatarStyle = {
width: 24,
height: 24,
borderRadius: '50%',
paddingRight: 4
}
export default class Info extends React.Component<InfoProps, InfoState> {
public constructor() {
super()
this.state = {userAvatarURL: ''}
}
public async componentWillMount() {
if (!this.props.user) {
return Promise.resolve()
}
const api = new Octokat({token: this.props.user.getToken()})
const user = await api.user.fetch()
this.setState({userAvatarURL: user.avatarUrl})
console.log('user', user)
return Promise.resolve()
}
private renderNoSelection() {
return (
<div>
<div>No row selected!</div>
</div>
)
}
private renderUser() {
return (
<div>
<img style={AvatarStyle} src={this.state.userAvatarURL}/>
<div>No repo selected!</div>
</div>
)
}
public render() {
const row = this.props.selectedRow
if (row < 0) {
const repo = this.props.selectedRepo
if (!repo) {
return this.renderNoSelection()
}
const img = LOLZ[row % LOLZ.length]
return (
<div style={ContainerStyle}>
<div style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}>
{this.renderUser()}
<div>Row {row + 1} is selected!</div>
</div>
Stars: {repo.stargazersCount}
<div style={ImageStyle}>
<img src={img}/>
</div>
<button onClick={() => shell.openExternal(repo.htmlUrl)}>Open</button>
</div>
)
}

36
src/lib/api.ts Normal file
View file

@ -0,0 +1,36 @@
import User from '../user'
const Octokat = require('octokat')
export interface Repo {
cloneUrl: string,
htmlUrl: string,
name: string
owner: {
avatarUrl: string,
login: string
type: 'user' | 'org'
},
private: boolean,
stargazersCount: number
}
export default class API {
private client: any
public constructor(user: User) {
this.client = new Octokat({token: user.getToken(), rootURL: user.getEndpoint()})
}
public async fetchRepos(): Promise<Repo[]> {
const results: Repo[] = []
let nextPage = this.client.user.repos
while (nextPage) {
const request = await nextPage.fetch()
results.push(...request.items)
nextPage = request.nextPage
}
return results
}
}

72
src/repos-list.tsx Normal file
View file

@ -0,0 +1,72 @@
import * as React from 'react'
import List from './list'
import User from './user'
import {Repo} from './lib/api'
interface ReposListProps {
selectedRow: number,
onSelectionChanged: (row: number) => void,
user: User,
loading: boolean,
repos: Repo[]
}
const RowHeight = 44
export default class ReposList extends React.Component<ReposListProps, void> {
private renderRow(row: number): JSX.Element {
const selected = row === this.props.selectedRow
const repo = this.props.repos[row]
const rowStyle = {
display: 'flex',
flexDirection: 'column',
padding: 4,
backgroundColor: selected ? 'blue' : 'white',
color: selected ? 'white' : 'black',
height: RowHeight
}
const titleStyle = {
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden'
}
const whiteness = 140
const ownerStyle = {
fontSize: '0.8em',
color: selected ? 'white' : `rgba(${whiteness}, ${whiteness}, ${whiteness}, 1)`
}
return (
<div style={rowStyle} key={row.toString()}>
<div style={titleStyle} title={repo.name}>{repo.name}</div>
<div style={ownerStyle}>
by {repo.owner.login} <img src={repo.owner.avatarUrl} style={{width: 12, height: 12, borderRadius: '50%'}}/>
</div>
</div>
)
}
private renderLoading() {
return (
<div>Loading</div>
)
}
public render() {
if (this.props.loading) {
return this.renderLoading()
}
return (
<List itemCount={this.props.repos.length}
itemHeight={RowHeight}
renderItem={row => this.renderRow(row)}
selectedRow={this.props.selectedRow}
onSelectionChanged={row => this.props.onSelectionChanged(row)}
style={{width: 120}}/>
)
}
}

View file

@ -1,49 +0,0 @@
import * as React from 'react'
import List from './list'
type ThingListProps = {
selectedRow: number,
onSelectionChanged: (row: number) => void
}
const RowHeight = 44
export default class ThingList extends React.Component<ThingListProps, void> {
public constructor(props: ThingListProps) {
super(props)
}
private renderRow(row: number): JSX.Element {
const selected = row === this.props.selectedRow
const inlineStyle = {
display: 'flex',
flexDirection: 'column',
padding: 4,
backgroundColor: selected ? 'blue' : 'white',
color: selected ? 'white' : 'black',
height: RowHeight
}
const whiteness = 140
return (
<div style={inlineStyle} key={row.toString()}>
<div>Item {row + 1}</div>
<div style={{
fontSize: '0.8em',
color: selected ? 'white' : `rgba(${whiteness}, ${whiteness}, ${whiteness}, 1)`
}}>Some subtitle</div>
</div>
)
}
public render() {
return (
<List itemCount={10000}
itemHeight={RowHeight}
renderItem={row => this.renderRow(row)}
selectedRow={this.props.selectedRow}
onSelectionChanged={row => this.props.onSelectionChanged(row)}
style={{width: 120}}/>
)
}
}