Skip to content

Commit

Permalink
Merge pull request #1126 from desktop/list-some-branches
Browse files Browse the repository at this point in the history
List some branches
  • Loading branch information
joshaber committed Apr 5, 2017
2 parents f08f3cc + 8b70b50 commit 54b05e1
Show file tree
Hide file tree
Showing 14 changed files with 613 additions and 182 deletions.
22 changes: 22 additions & 0 deletions app/src/lib/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,31 @@ export interface IRepositoryState {
}

export interface IBranchesState {
/**
* The current tip of HEAD, either a branch, a commit (if HEAD is
* detached) or an unborn branch (a branch with no commits).
*/
readonly tip: Tip

/**
* The default branch for a given repository. Most commonly this
* will be the 'master' branch but GitHub users are able to change
* their default branch in the web UI.
*/
readonly defaultBranch: Branch | null

/**
* A list of all branches (remote and local) that's currently in
* the repository.
*/
readonly allBranches: ReadonlyArray<Branch>

/**
* A list of zero to a few (at time of writing 5 but check loadRecentBranches
* in git-store for definitive answer) branches that have been checked out
* recently. This list is compiled by reading the reflog and tracking branch
* switches over the last couple of thousand reflog entries.
*/
readonly recentBranches: ReadonlyArray<Branch>
}

Expand Down
2 changes: 2 additions & 0 deletions app/src/main-process/app-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export class AppWindow {
// Disable auxclick event
// See https://developers.google.com/web/updates/2016/10/auxclick
disableBlinkFeatures: 'Auxclick',
// Enable, among other things, the ResizeObserver
experimentalFeatures: true,
},
}

Expand Down
13 changes: 11 additions & 2 deletions app/src/ui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -733,11 +733,20 @@ export class App extends React.Component<IAppProps, IAppState> {
const repository = popup.repository
const state = this.props.appStore.getRepositoryState(repository)

const tip = state.branchesState.tip
const currentBranch = tip.kind === TipState.Valid
? tip.branch
: null

return <Merge
dispatcher={this.props.dispatcher}
repository={repository}
branches={state.branchesState.allBranches}
onDismissed={this.onPopupDismissed}/>
allBranches={state.branchesState.allBranches}
defaultBranch={state.branchesState.defaultBranch}
recentBranches={state.branchesState.recentBranches}
currentBranch={currentBranch}
onDismissed={this.onPopupDismissed}
/>
}
case PopupType.RepositorySettings: {
const repository = popup.repository
Expand Down
155 changes: 155 additions & 0 deletions app/src/ui/branches/branch-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as React from 'react'
import { Branch } from '../../models/branch'
import { groupBranches, IBranchListItem, BranchGroupIdentifier } from './group-branches'
import { BranchListItem } from './branch'
import { FilterList, IFilterListGroup, SelectionSource } from '../lib/filter-list'
import { assertNever } from '../../lib/fatal-error'

/**
* TS can't parse generic specialization in JSX, so we have to alias it here
* with the generic type. See https://github.com/Microsoft/TypeScript/issues/6395.
*/
const BranchesFilterList: new() => FilterList<IBranchListItem> = FilterList as any

const RowHeight = 30

interface IBranchListProps {

/**
* See IBranchesState.defaultBranch
*/
readonly defaultBranch: Branch | null

/**
* The currently checked out branch or null if HEAD is detached
*/
readonly currentBranch: Branch | null

/**
* See IBranchesState.allBranches
*/
readonly allBranches: ReadonlyArray<Branch>

/**
* See IBranchesState.recentBranches
*/
readonly recentBranches: ReadonlyArray<Branch>

/**
* The currently selected branch in the list, see the onSelectionChanged prop.
*/
readonly selectedBranch: Branch | null

/**
* Called when a key down happens in the filter field. Users have a chance to
* respond or cancel the default behavior by calling `preventDefault`.
*/
readonly onFilterKeyDown?: (filter: string, event: React.KeyboardEvent<HTMLInputElement>) => void

/** Called when an item is clicked. */
readonly onItemClick?: (item: Branch) => void

/**
* This function will be called when the selection changes as a result of a
* user keyboard or mouse action (i.e. not when props change). Note that this
* differs from `onRowSelected`. For example, it won't be called if an already
* selected row is clicked on.
*
* @param selectedItem - The Branch that was just selected
* @param source - The kind of user action that provoked the change,
* either a pointer device press, or a keyboard event
* (arrow up/down)
*/
readonly onSelectionChanged?: (selectedItem: Branch | null, source: SelectionSource) => void
}

interface IBranchListState {
readonly groups: ReadonlyArray<IFilterListGroup<IBranchListItem>>
readonly selectedItem: IBranchListItem | null
}

function createState(props: IBranchListProps): IBranchListState {
const groups = groupBranches(props.defaultBranch, props.currentBranch, props.allBranches, props.recentBranches)

let selectedItem: IBranchListItem | null = null
const selectedBranch = props.selectedBranch
if (selectedBranch) {
for (const group of groups) {
selectedItem = group.items.find(i => {
const branch = i.branch
return branch.name === selectedBranch.name
}) || null

if (selectedItem) { break }
}
}

return { groups, selectedItem }
}

/** The Branches list component. */
export class BranchList extends React.Component<IBranchListProps, IBranchListState> {

public constructor(props: IBranchListProps) {
super(props)
this.state = createState(props)
}

private renderItem = (item: IBranchListItem) => {
const branch = item.branch
const commit = branch.tip
const currentBranchName = this.props.currentBranch ? this.props.currentBranch.name : null
return <BranchListItem
name={branch.name}
isCurrentBranch={branch.name === currentBranchName}
lastCommitDate={commit ? commit.author.date : null}/>
}

private getGroupLabel(identifier: BranchGroupIdentifier) {
if (identifier === 'default') {
return __DARWIN__ ? 'Default Branch' : 'Default branch'
} else if (identifier === 'recent') {
return __DARWIN__ ? 'Recent Branches' : 'Recent branches'
} else if (identifier === 'other') {
return __DARWIN__ ? 'Other Branches' : 'Other branches'
} else {
return assertNever(identifier, `Unknown identifier: ${identifier}`)
}
}

private renderGroupHeader = (identifier: BranchGroupIdentifier) => {
return <div className='branches-list-content branches-list-label'>{this.getGroupLabel(identifier)}</div>
}

private onItemClick = (item: IBranchListItem) => {
if (this.props.onItemClick) {
this.props.onItemClick(item.branch)
}
}

private onSelectionChanged = (selectedItem: IBranchListItem | null, source: SelectionSource) => {
if (this.props.onSelectionChanged) {
this.props.onSelectionChanged(selectedItem ? selectedItem.branch : null, source)
}
}

public componentWillReceiveProps(nextProps: IBranchListProps) {
this.setState(createState(nextProps))
}

public render() {
return (
<BranchesFilterList
className='branches-list'
rowHeight={RowHeight}
selectedItem={this.state.selectedItem}
renderItem={this.renderItem}
renderGroupHeader={this.renderGroupHeader}
onItemClick={this.onItemClick}
onFilterKeyDown={this.props.onFilterKeyDown}
onSelectionChanged={this.onSelectionChanged}
groups={this.state.groups}
invalidationProps={this.props.allBranches}/>
)
}
}
89 changes: 28 additions & 61 deletions app/src/ui/branches/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,7 @@ import * as React from 'react'
import { Dispatcher } from '../../lib/dispatcher'
import { Repository } from '../../models/repository'
import { Branch } from '../../models/branch'
import { groupBranches, IBranchListItem, BranchGroupIdentifier } from './group-branches'
import { BranchListItem } from './branch'
import { FilterList } from '../lib/filter-list'
import { assertNever } from '../../lib/fatal-error'

/**
* TS can't parse generic specialization in JSX, so we have to alias it here
* with the generic type. See https://github.com/Microsoft/TypeScript/issues/6395.
*/
const BranchesFilterList: new() => FilterList<IBranchListItem> = FilterList as any

const RowHeight = 30
import { BranchList } from './branch-list'

interface IBranchesProps {
readonly defaultBranch: Branch | null
Expand All @@ -24,38 +13,27 @@ interface IBranchesProps {
readonly repository: Repository
}

interface IBranchesState {
readonly selectedBranch: Branch | null
}

/** The Branches list component. */
export class Branches extends React.Component<IBranchesProps, void> {
private renderItem = (item: IBranchListItem) => {
const branch = item.branch
const commit = branch.tip
const currentBranchName = this.props.currentBranch ? this.props.currentBranch.name : null
return <BranchListItem
name={branch.name}
isCurrentBranch={branch.name === currentBranchName}
lastCommitDate={commit ? commit.author.date : null}/>
}
export class Branches extends React.Component<IBranchesProps, IBranchesState> {

private getGroupLabel(identifier: BranchGroupIdentifier) {
if (identifier === 'default') {
return 'Default Branch'
} else if (identifier === 'recent') {
return 'Recent Branches'
} else if (identifier === 'other') {
return 'Other Branches'
} else {
return assertNever(identifier, `Unknown identifier: ${identifier}`)
}
}
public constructor(props: IBranchesProps) {
super(props)

private renderGroupHeader = (identifier: BranchGroupIdentifier) => {
return <div className='branches-list-content branches-list-label'>{this.getGroupLabel(identifier)}</div>
this.state = { selectedBranch: props.currentBranch }
}

private onItemClick = (item: IBranchListItem) => {
const branch = item.branch
private onItemClick = (item: Branch) => {
this.props.dispatcher.closeFoldout()
this.props.dispatcher.checkoutBranch(this.props.repository, branch.nameWithoutRemote)

const currentBranch = this.props.currentBranch

if (!currentBranch || currentBranch.name !== item.name) {
this.props.dispatcher.checkoutBranch(this.props.repository, item.nameWithoutRemote)
}
}

private onFilterKeyDown = (filter: string, event: React.KeyboardEvent<HTMLInputElement>) => {
Expand All @@ -67,34 +45,23 @@ export class Branches extends React.Component<IBranchesProps, void> {
}
}

public render() {
const groups = groupBranches(this.props.defaultBranch, this.props.currentBranch, this.props.allBranches, this.props.recentBranches)

let selectedItem: IBranchListItem | null = null
const currentBranch = this.props.currentBranch
if (currentBranch) {
for (const group of groups) {
selectedItem = group.items.find(i => {
const branch = i.branch
return branch.name === currentBranch.name
}) || null

if (selectedItem) { break }
}
}
private onSelectionChanged = (selectedBranch: Branch) => {
this.setState({ selectedBranch })
}

public render() {
return (
<div className='branches-list-container'>
<BranchesFilterList
className='branches-list'
rowHeight={RowHeight}
selectedItem={selectedItem}
renderItem={this.renderItem}
renderGroupHeader={this.renderGroupHeader}
<BranchList
defaultBranch={this.props.defaultBranch}
currentBranch={this.props.currentBranch}
allBranches={this.props.allBranches}
recentBranches={this.props.recentBranches}
onItemClick={this.onItemClick}
onFilterKeyDown={this.onFilterKeyDown}
groups={groups}
invalidationProps={this.props.allBranches}/>
selectedBranch={this.state.selectedBranch}
onSelectionChanged={this.onSelectionChanged}
/>
</div>
)
}
Expand Down
Loading

0 comments on commit 54b05e1

Please sign in to comment.