mirror of
https://github.com/desktop/desktop
synced 2024-09-13 21:31:32 +00:00
Merge branch 'development' into reuse-workflow
This commit is contained in:
commit
9a6d45122f
|
@ -96,3 +96,7 @@ export function enablePullRequestQuickView(): boolean {
|
||||||
export function enableMoveStash(): boolean {
|
export function enableMoveStash(): boolean {
|
||||||
return enableBetaFeatures()
|
return enableBetaFeatures()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function enableSectionList(): boolean {
|
||||||
|
return enableDevelopmentFeatures()
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { RowIndexPath } from '../ui/lib/list/list-row-index-path'
|
||||||
import { Commit } from './commit'
|
import { Commit } from './commit'
|
||||||
import { GitHubRepository } from './github-repository'
|
import { GitHubRepository } from './github-repository'
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ export type CommitTarget = {
|
||||||
export type ListInsertionPointTarget = {
|
export type ListInsertionPointTarget = {
|
||||||
type: DropTargetType.ListInsertionPoint
|
type: DropTargetType.ListInsertionPoint
|
||||||
data: DragData
|
data: DragData
|
||||||
index: number
|
index: RowIndexPath
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,6 +22,8 @@ import { NoBranches } from './no-branches'
|
||||||
import { SelectionDirection, ClickSource } from '../lib/list'
|
import { SelectionDirection, ClickSource } from '../lib/list'
|
||||||
import { generateBranchContextMenuItems } from './branch-list-item-context-menu'
|
import { generateBranchContextMenuItems } from './branch-list-item-context-menu'
|
||||||
import { showContextualMenu } from '../../lib/menu-item'
|
import { showContextualMenu } from '../../lib/menu-item'
|
||||||
|
import { enableSectionList } from '../../lib/feature-flag'
|
||||||
|
import { SectionFilterList } from '../lib/section-filter-list'
|
||||||
|
|
||||||
const RowHeight = 30
|
const RowHeight = 30
|
||||||
|
|
||||||
|
@ -168,7 +170,10 @@ export class BranchList extends React.Component<
|
||||||
IBranchListProps,
|
IBranchListProps,
|
||||||
IBranchListState
|
IBranchListState
|
||||||
> {
|
> {
|
||||||
private branchFilterList: FilterList<IBranchListItem> | null = null
|
private branchFilterList:
|
||||||
|
| FilterList<IBranchListItem>
|
||||||
|
| SectionFilterList<IBranchListItem>
|
||||||
|
| null = null
|
||||||
|
|
||||||
public constructor(props: IBranchListProps) {
|
public constructor(props: IBranchListProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
@ -186,7 +191,32 @@ export class BranchList extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return enableSectionList() ? (
|
||||||
|
<SectionFilterList<IBranchListItem>
|
||||||
|
ref={this.onBranchesFilterListRef}
|
||||||
|
className="branches-list"
|
||||||
|
rowHeight={RowHeight}
|
||||||
|
filterText={this.props.filterText}
|
||||||
|
onFilterTextChanged={this.props.onFilterTextChanged}
|
||||||
|
onFilterKeyDown={this.props.onFilterKeyDown}
|
||||||
|
selectedItem={this.state.selectedItem}
|
||||||
|
renderItem={this.renderItem}
|
||||||
|
renderGroupHeader={this.renderGroupHeader}
|
||||||
|
onItemClick={this.onItemClick}
|
||||||
|
onSelectionChanged={this.onSelectionChanged}
|
||||||
|
onEnterPressedWithoutFilteredItems={this.onCreateNewBranch}
|
||||||
|
groups={this.state.groups}
|
||||||
|
invalidationProps={this.props.allBranches}
|
||||||
|
renderPostFilter={this.onRenderNewButton}
|
||||||
|
renderNoItems={this.onRenderNoItems}
|
||||||
|
filterTextBox={this.props.textbox}
|
||||||
|
hideFilterRow={this.props.hideFilterRow}
|
||||||
|
onFilterListResultsChanged={this.props.onFilterListResultsChanged}
|
||||||
|
renderPreList={this.props.renderPreList}
|
||||||
|
onItemContextMenu={this.onBranchContextMenu}
|
||||||
|
getGroupAriaLabel={this.getGroupAriaLabel}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<FilterList<IBranchListItem>
|
<FilterList<IBranchListItem>
|
||||||
ref={this.onBranchesFilterListRef}
|
ref={this.onBranchesFilterListRef}
|
||||||
className="branches-list"
|
className="branches-list"
|
||||||
|
@ -237,7 +267,10 @@ export class BranchList extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
private onBranchesFilterListRef = (
|
private onBranchesFilterListRef = (
|
||||||
filterList: FilterList<IBranchListItem> | null
|
filterList:
|
||||||
|
| FilterList<IBranchListItem>
|
||||||
|
| SectionFilterList<IBranchListItem>
|
||||||
|
| null
|
||||||
) => {
|
) => {
|
||||||
this.branchFilterList = filterList
|
this.branchFilterList = filterList
|
||||||
}
|
}
|
||||||
|
@ -257,6 +290,15 @@ export class BranchList extends React.Component<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getGroupAriaLabel = (group: number) => {
|
||||||
|
const GroupIdentifiers: ReadonlyArray<BranchGroupIdentifier> = [
|
||||||
|
'default',
|
||||||
|
'recent',
|
||||||
|
'other',
|
||||||
|
]
|
||||||
|
return this.getGroupLabel(GroupIdentifiers[group])
|
||||||
|
}
|
||||||
|
|
||||||
private renderGroupHeader = (label: string) => {
|
private renderGroupHeader = (label: string) => {
|
||||||
const identifier = this.parseHeader(label)
|
const identifier = this.parseHeader(label)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ import { HighlightText } from '../lib/highlight-text'
|
||||||
import { ClickSource } from '../lib/list'
|
import { ClickSource } from '../lib/list'
|
||||||
import { LinkButton } from '../lib/link-button'
|
import { LinkButton } from '../lib/link-button'
|
||||||
import { Ref } from '../lib/ref'
|
import { Ref } from '../lib/ref'
|
||||||
|
import { enableSectionList } from '../../lib/feature-flag'
|
||||||
|
import { SectionFilterList } from '../lib/section-filter-list'
|
||||||
|
|
||||||
interface ICloneableRepositoryFilterListProps {
|
interface ICloneableRepositoryFilterListProps {
|
||||||
/** The account to clone from. */
|
/** The account to clone from. */
|
||||||
|
@ -157,26 +159,34 @@ export class CloneableRepositoryFilterList extends React.PureComponent<ICloneabl
|
||||||
const { repositories, account, selectedItem } = this.props
|
const { repositories, account, selectedItem } = this.props
|
||||||
|
|
||||||
const groups = this.getRepositoryGroups(repositories, account.login)
|
const groups = this.getRepositoryGroups(repositories, account.login)
|
||||||
const selectedListItem = this.getSelectedListItem(groups, selectedItem)
|
const getGroupAriaLabel = (group: number) => {
|
||||||
|
const groupIdentifier = groups[group].identifier
|
||||||
|
return groupIdentifier === YourRepositoriesIdentifier
|
||||||
|
? this.getYourRepositoriesLabel()
|
||||||
|
: groupIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const selectedListItem = this.getSelectedListItem(groups, selectedItem)
|
||||||
<FilterList<ICloneableRepositoryListItem>
|
const ListComponent = enableSectionList() ? SectionFilterList : FilterList
|
||||||
className="clone-github-repo"
|
const filterListProps: typeof ListComponent['prototype']['props'] = {
|
||||||
rowHeight={RowHeight}
|
className: 'clone-github-repo',
|
||||||
selectedItem={selectedListItem}
|
rowHeight: RowHeight,
|
||||||
renderItem={this.renderItem}
|
selectedItem: selectedListItem,
|
||||||
renderGroupHeader={this.renderGroupHeader}
|
renderItem: this.renderItem,
|
||||||
onSelectionChanged={this.onSelectionChanged}
|
renderGroupHeader: this.renderGroupHeader,
|
||||||
invalidationProps={groups}
|
onSelectionChanged: this.onSelectionChanged,
|
||||||
groups={groups}
|
invalidationProps: groups,
|
||||||
filterText={this.props.filterText}
|
groups: groups,
|
||||||
onFilterTextChanged={this.props.onFilterTextChanged}
|
filterText: this.props.filterText,
|
||||||
renderNoItems={this.renderNoItems}
|
onFilterTextChanged: this.props.onFilterTextChanged,
|
||||||
renderPostFilter={this.renderPostFilter}
|
renderNoItems: this.renderNoItems,
|
||||||
onItemClick={this.props.onItemClicked ? this.onItemClick : undefined}
|
renderPostFilter: this.renderPostFilter,
|
||||||
placeholderText="Filter your repositories"
|
onItemClick: this.props.onItemClicked ? this.onItemClick : undefined,
|
||||||
/>
|
placeholderText: 'Filter your repositories',
|
||||||
)
|
getGroupAriaLabel,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ListComponent {...filterListProps} />
|
||||||
}
|
}
|
||||||
|
|
||||||
private onItemClick = (
|
private onItemClick = (
|
||||||
|
@ -206,10 +216,14 @@ export class CloneableRepositoryFilterList extends React.PureComponent<ICloneabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getYourRepositoriesLabel = () => {
|
||||||
|
return __DARWIN__ ? 'Your Repositories' : 'Your repositories'
|
||||||
|
}
|
||||||
|
|
||||||
private renderGroupHeader = (identifier: string) => {
|
private renderGroupHeader = (identifier: string) => {
|
||||||
let header = identifier
|
let header = identifier
|
||||||
if (identifier === YourRepositoriesIdentifier) {
|
if (identifier === YourRepositoriesIdentifier) {
|
||||||
header = __DARWIN__ ? 'Your Repositories' : 'Your repositories'
|
header = this.getYourRepositoriesLabel()
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="clone-repository-list-content clone-repository-list-group-header">
|
<div className="clone-repository-list-content clone-repository-list-group-header">
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Disposable } from 'event-kit'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { dragAndDropManager } from '../../../lib/drag-and-drop-manager'
|
import { dragAndDropManager } from '../../../lib/drag-and-drop-manager'
|
||||||
import { DragData, DragType, DropTargetType } from '../../../models/drag-drop'
|
import { DragData, DragType, DropTargetType } from '../../../models/drag-drop'
|
||||||
|
import { RowIndexPath } from './list-row-index-path'
|
||||||
|
|
||||||
enum InsertionFeedbackType {
|
enum InsertionFeedbackType {
|
||||||
None,
|
None,
|
||||||
|
@ -13,11 +14,11 @@ enum InsertionFeedbackType {
|
||||||
|
|
||||||
interface IListItemInsertionOverlayProps {
|
interface IListItemInsertionOverlayProps {
|
||||||
readonly onDropDataInsertion?: (
|
readonly onDropDataInsertion?: (
|
||||||
insertionIndex: number,
|
insertionIndex: RowIndexPath,
|
||||||
data: DragData
|
data: DragData
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
readonly itemIndex: number
|
readonly itemIndex: RowIndexPath
|
||||||
readonly dragType: DragType
|
readonly dragType: DragType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +189,10 @@ export class ListItemInsertionOverlay extends React.PureComponent<
|
||||||
let index = this.props.itemIndex
|
let index = this.props.itemIndex
|
||||||
|
|
||||||
if (this.state.feedbackType === InsertionFeedbackType.Bottom) {
|
if (this.state.feedbackType === InsertionFeedbackType.Bottom) {
|
||||||
index++
|
index = {
|
||||||
|
...index,
|
||||||
|
row: index.row + 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.props.onDropDataInsertion(index, dragAndDropManager.dragData)
|
this.props.onDropDataInsertion(index, dragAndDropManager.dragData)
|
||||||
}
|
}
|
||||||
|
|
92
app/src/ui/lib/list/list-row-index-path.ts
Normal file
92
app/src/ui/lib/list/list-row-index-path.ts
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
export type RowIndexPath = {
|
||||||
|
readonly row: number
|
||||||
|
readonly section: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvalidRowIndexPath: RowIndexPath = { section: -1, row: -1 }
|
||||||
|
|
||||||
|
export function rowIndexPathEquals(a: RowIndexPath, b: RowIndexPath): boolean {
|
||||||
|
return a.section === b.section && a.row === b.row
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTotalRowCount(rowCount: ReadonlyArray<number>) {
|
||||||
|
return rowCount.reduce((sum, count) => sum + count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rowIndexPathToGlobalIndex(
|
||||||
|
indexPath: RowIndexPath,
|
||||||
|
rowCount: ReadonlyArray<number>
|
||||||
|
): number | null {
|
||||||
|
if (!isValidRow(indexPath, rowCount)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
for (let section = 0; section < indexPath.section; section++) {
|
||||||
|
index += rowCount[section]
|
||||||
|
}
|
||||||
|
|
||||||
|
index += indexPath.row
|
||||||
|
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
export function globalIndexToRowIndexPath(
|
||||||
|
index: number,
|
||||||
|
rowCount: ReadonlyArray<number>
|
||||||
|
): RowIndexPath | null {
|
||||||
|
if (index < 0 || index >= getTotalRowCount(rowCount)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let section = 0
|
||||||
|
let row = index
|
||||||
|
|
||||||
|
while (row >= rowCount[section]) {
|
||||||
|
row -= rowCount[section]
|
||||||
|
section++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { section, row }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidRow(
|
||||||
|
indexPath: RowIndexPath,
|
||||||
|
rowCount: ReadonlyArray<number>
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
indexPath.section >= 0 &&
|
||||||
|
indexPath.section < rowCount.length &&
|
||||||
|
indexPath.row >= 0 &&
|
||||||
|
indexPath.row < rowCount[indexPath.section]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFirstRowIndexPath(
|
||||||
|
rowCount: ReadonlyArray<number>
|
||||||
|
): RowIndexPath | null {
|
||||||
|
if (rowCount.length > 0) {
|
||||||
|
for (let section = 0; section < rowCount.length; section++) {
|
||||||
|
if (rowCount[section] > 0) {
|
||||||
|
return { section, row: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLastRowIndexPath(
|
||||||
|
rowCount: ReadonlyArray<number>
|
||||||
|
): RowIndexPath | null {
|
||||||
|
if (rowCount.length > 0) {
|
||||||
|
for (let section = rowCount.length - 1; section >= 0; section--) {
|
||||||
|
if (rowCount[section] > 0) {
|
||||||
|
return { section, row: rowCount[section] - 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
|
@ -1,12 +1,16 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import { RowIndexPath } from './list-row-index-path'
|
||||||
|
|
||||||
interface IListRowProps {
|
interface IListRowProps {
|
||||||
|
/** whether or not the section to which this row belongs has a header */
|
||||||
|
readonly sectionHasHeader: boolean
|
||||||
|
|
||||||
/** the total number of row in this list */
|
/** the total number of row in this list */
|
||||||
readonly rowCount: number
|
readonly rowCount: number
|
||||||
|
|
||||||
/** the index of the row in the list */
|
/** the index of the row in the list */
|
||||||
readonly rowIndex: number
|
readonly rowIndex: RowIndexPath
|
||||||
|
|
||||||
/** custom styles to provide to the row */
|
/** custom styles to provide to the row */
|
||||||
readonly style?: React.CSSProperties
|
readonly style?: React.CSSProperties
|
||||||
|
@ -21,39 +25,51 @@ interface IListRowProps {
|
||||||
readonly selected?: boolean
|
readonly selected?: boolean
|
||||||
|
|
||||||
/** callback to fire when the DOM element is created */
|
/** callback to fire when the DOM element is created */
|
||||||
readonly onRowRef?: (index: number, element: HTMLDivElement | null) => void
|
readonly onRowRef?: (
|
||||||
|
index: RowIndexPath,
|
||||||
|
element: HTMLDivElement | null
|
||||||
|
) => void
|
||||||
|
|
||||||
/** callback to fire when the row receives a mousedown event */
|
/** callback to fire when the row receives a mousedown event */
|
||||||
readonly onRowMouseDown: (index: number, e: React.MouseEvent<any>) => void
|
readonly onRowMouseDown: (
|
||||||
|
index: RowIndexPath,
|
||||||
|
e: React.MouseEvent<any>
|
||||||
|
) => void
|
||||||
|
|
||||||
/** callback to fire when the row receives a mouseup event */
|
/** callback to fire when the row receives a mouseup event */
|
||||||
readonly onRowMouseUp: (index: number, e: React.MouseEvent<any>) => void
|
readonly onRowMouseUp: (index: RowIndexPath, e: React.MouseEvent<any>) => void
|
||||||
|
|
||||||
/** callback to fire when the row is clicked */
|
/** callback to fire when the row is clicked */
|
||||||
readonly onRowClick: (index: number, e: React.MouseEvent<any>) => void
|
readonly onRowClick: (index: RowIndexPath, e: React.MouseEvent<any>) => void
|
||||||
|
|
||||||
/** callback to fire when the row is double clicked */
|
/** callback to fire when the row is double clicked */
|
||||||
readonly onRowDoubleClick: (index: number, e: React.MouseEvent<any>) => void
|
readonly onRowDoubleClick: (
|
||||||
|
index: RowIndexPath,
|
||||||
|
e: React.MouseEvent<any>
|
||||||
|
) => void
|
||||||
|
|
||||||
/** callback to fire when the row receives a keyboard event */
|
/** callback to fire when the row receives a keyboard event */
|
||||||
readonly onRowKeyDown: (index: number, e: React.KeyboardEvent<any>) => void
|
readonly onRowKeyDown: (
|
||||||
|
index: RowIndexPath,
|
||||||
|
e: React.KeyboardEvent<any>
|
||||||
|
) => void
|
||||||
|
|
||||||
/** called when the row (or any of its descendants) receives focus */
|
/** called when the row (or any of its descendants) receives focus */
|
||||||
readonly onRowFocus?: (
|
readonly onRowFocus?: (
|
||||||
index: number,
|
index: RowIndexPath,
|
||||||
e: React.FocusEvent<HTMLDivElement>
|
e: React.FocusEvent<HTMLDivElement>
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
/** called when the row (and all of its descendants) loses focus */
|
/** called when the row (and all of its descendants) loses focus */
|
||||||
readonly onRowBlur?: (
|
readonly onRowBlur?: (
|
||||||
index: number,
|
index: RowIndexPath,
|
||||||
e: React.FocusEvent<HTMLDivElement>
|
e: React.FocusEvent<HTMLDivElement>
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
/** Called back for when the context menu is invoked (user right clicks of
|
/** Called back for when the context menu is invoked (user right clicks of
|
||||||
* uses keyboard shortcuts) */
|
* uses keyboard shortcuts) */
|
||||||
readonly onContextMenu?: (
|
readonly onContextMenu?: (
|
||||||
index: number,
|
index: RowIndexPath,
|
||||||
e: React.MouseEvent<HTMLDivElement>
|
e: React.MouseEvent<HTMLDivElement>
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
|
@ -106,12 +122,23 @@ export class ListRow extends React.Component<IListRowProps, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const selected = this.props.selected
|
const {
|
||||||
const className = classNames(
|
selected,
|
||||||
|
selectable,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
rowCount,
|
||||||
|
id,
|
||||||
|
tabIndex,
|
||||||
|
rowIndex,
|
||||||
|
children,
|
||||||
|
sectionHasHeader,
|
||||||
|
} = this.props
|
||||||
|
const rowClassName = classNames(
|
||||||
'list-item',
|
'list-item',
|
||||||
{ selected },
|
{ selected },
|
||||||
{ 'not-selectable': this.props.selectable === false },
|
{ 'not-selectable': selectable === false },
|
||||||
this.props.className
|
className
|
||||||
)
|
)
|
||||||
// react-virtualized gives us an explicit pixel width for rows, but that
|
// react-virtualized gives us an explicit pixel width for rows, but that
|
||||||
// width doesn't take into account whether or not the scroll bar needs
|
// width doesn't take into account whether or not the scroll bar needs
|
||||||
|
@ -120,29 +147,43 @@ export class ListRow extends React.Component<IListRowProps, {}> {
|
||||||
// *But* the parent Grid uses `autoContainerWidth` which means its width
|
// *But* the parent Grid uses `autoContainerWidth` which means its width
|
||||||
// *does* reflect any width needed by the scroll bar. So we should just use
|
// *does* reflect any width needed by the scroll bar. So we should just use
|
||||||
// that width.
|
// that width.
|
||||||
const style = { ...this.props.style, width: '100%' }
|
const fullWidthStyle = { ...style, width: '100%' }
|
||||||
|
|
||||||
|
let ariaSetSize: number | undefined = rowCount
|
||||||
|
let ariaPosInSet: number | undefined = rowIndex.row + 1
|
||||||
|
if (sectionHasHeader) {
|
||||||
|
if (rowIndex.row === 0) {
|
||||||
|
ariaSetSize = undefined
|
||||||
|
ariaPosInSet = undefined
|
||||||
|
} else {
|
||||||
|
ariaSetSize -= 1
|
||||||
|
ariaPosInSet -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={this.props.id}
|
id={id}
|
||||||
role="option"
|
role={
|
||||||
aria-setsize={this.props.rowCount}
|
sectionHasHeader && rowIndex.row === 0 ? 'presentation' : 'option'
|
||||||
aria-posinset={this.props.rowIndex + 1}
|
}
|
||||||
aria-selected={this.props.selectable ? this.props.selected : undefined}
|
aria-setsize={ariaSetSize}
|
||||||
className={className}
|
aria-posinset={ariaPosInSet}
|
||||||
tabIndex={this.props.tabIndex}
|
aria-selected={selectable ? selected : undefined}
|
||||||
|
className={rowClassName}
|
||||||
|
tabIndex={tabIndex}
|
||||||
ref={this.onRef}
|
ref={this.onRef}
|
||||||
onMouseDown={this.onRowMouseDown}
|
onMouseDown={this.onRowMouseDown}
|
||||||
onMouseUp={this.onRowMouseUp}
|
onMouseUp={this.onRowMouseUp}
|
||||||
onClick={this.onRowClick}
|
onClick={this.onRowClick}
|
||||||
onDoubleClick={this.onRowDoubleClick}
|
onDoubleClick={this.onRowDoubleClick}
|
||||||
onKeyDown={this.onRowKeyDown}
|
onKeyDown={this.onRowKeyDown}
|
||||||
style={style}
|
style={fullWidthStyle}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { range } from '../../../lib/range'
|
||||||
import { ListItemInsertionOverlay } from './list-item-insertion-overlay'
|
import { ListItemInsertionOverlay } from './list-item-insertion-overlay'
|
||||||
import { DragData, DragType } from '../../../models/drag-drop'
|
import { DragData, DragType } from '../../../models/drag-drop'
|
||||||
import memoizeOne from 'memoize-one'
|
import memoizeOne from 'memoize-one'
|
||||||
|
import { RowIndexPath } from './list-row-index-path'
|
||||||
import { sendNonFatalException } from '../../../lib/helpers/non-fatal-exception'
|
import { sendNonFatalException } from '../../../lib/helpers/non-fatal-exception'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -580,11 +581,11 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRowKeyDown = (
|
private onRowKeyDown = (
|
||||||
rowIndex: number,
|
indexPath: RowIndexPath,
|
||||||
event: React.KeyboardEvent<any>
|
event: React.KeyboardEvent<any>
|
||||||
) => {
|
) => {
|
||||||
if (this.props.onRowKeyDown) {
|
if (this.props.onRowKeyDown) {
|
||||||
this.props.onRowKeyDown(rowIndex, event)
|
this.props.onRowKeyDown(indexPath.row, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasModifier =
|
const hasModifier =
|
||||||
|
@ -645,21 +646,27 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRowFocus = (index: number, e: React.FocusEvent<HTMLDivElement>) => {
|
private onRowFocus = (
|
||||||
this.focusRow = index
|
indexPath: RowIndexPath,
|
||||||
|
e: React.FocusEvent<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
this.focusRow = indexPath.row
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRowBlur = (index: number, e: React.FocusEvent<HTMLDivElement>) => {
|
private onRowBlur = (
|
||||||
if (this.focusRow === index) {
|
indexPath: RowIndexPath,
|
||||||
|
e: React.FocusEvent<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
if (this.focusRow === indexPath.row) {
|
||||||
this.focusRow = -1
|
this.focusRow = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRowContextMenu = (
|
private onRowContextMenu = (
|
||||||
row: number,
|
indexPath: RowIndexPath,
|
||||||
e: React.MouseEvent<HTMLDivElement>
|
e: React.MouseEvent<HTMLDivElement>
|
||||||
) => {
|
) => {
|
||||||
this.props.onRowContextMenu?.(row, e)
|
this.props.onRowContextMenu?.(indexPath.row, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convenience method for invoking canSelectRow callback when it exists */
|
/** Convenience method for invoking canSelectRow callback when it exists */
|
||||||
|
@ -867,14 +874,17 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRowRef = (rowIndex: number, element: HTMLDivElement | null) => {
|
private onRowRef = (
|
||||||
|
indexPath: RowIndexPath,
|
||||||
|
element: HTMLDivElement | null
|
||||||
|
) => {
|
||||||
if (element === null) {
|
if (element === null) {
|
||||||
this.rowRefs.delete(rowIndex)
|
this.rowRefs.delete(indexPath.row)
|
||||||
} else {
|
} else {
|
||||||
this.rowRefs.set(rowIndex, element)
|
this.rowRefs.set(indexPath.row, element)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rowIndex === this.focusRow) {
|
if (indexPath.row === this.focusRow) {
|
||||||
// The currently focused row is going being unmounted so we'll move focus
|
// The currently focused row is going being unmounted so we'll move focus
|
||||||
// programmatically to the grid so that keyboard navigation still works
|
// programmatically to the grid so that keyboard navigation still works
|
||||||
if (element === null) {
|
if (element === null) {
|
||||||
|
@ -925,8 +935,8 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
const element =
|
const element =
|
||||||
this.props.insertionDragType !== undefined ? (
|
this.props.insertionDragType !== undefined ? (
|
||||||
<ListItemInsertionOverlay
|
<ListItemInsertionOverlay
|
||||||
onDropDataInsertion={this.props.onDropDataInsertion}
|
onDropDataInsertion={this.onDropDataInsertion}
|
||||||
itemIndex={rowIndex}
|
itemIndex={{ section: 0, row: rowIndex }}
|
||||||
dragType={this.props.insertionDragType}
|
dragType={this.props.insertionDragType}
|
||||||
>
|
>
|
||||||
{row}
|
{row}
|
||||||
|
@ -943,7 +953,8 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
id={id}
|
id={id}
|
||||||
onRowRef={this.onRowRef}
|
onRowRef={this.onRowRef}
|
||||||
rowCount={this.props.rowCount}
|
rowCount={this.props.rowCount}
|
||||||
rowIndex={rowIndex}
|
rowIndex={{ section: 0, row: rowIndex }}
|
||||||
|
sectionHasHeader={false}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
onRowClick={this.onRowClick}
|
onRowClick={this.onRowClick}
|
||||||
onRowDoubleClick={this.onRowDoubleClick}
|
onRowDoubleClick={this.onRowDoubleClick}
|
||||||
|
@ -1144,7 +1155,12 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRowMouseDown = (row: number, event: React.MouseEvent<any>) => {
|
private onRowMouseDown = (
|
||||||
|
indexPath: RowIndexPath,
|
||||||
|
event: React.MouseEvent<any>
|
||||||
|
) => {
|
||||||
|
const { row } = indexPath
|
||||||
|
|
||||||
if (this.canSelectRow(row)) {
|
if (this.canSelectRow(row)) {
|
||||||
if (this.props.onRowMouseDown) {
|
if (this.props.onRowMouseDown) {
|
||||||
this.props.onRowMouseDown(row, event)
|
this.props.onRowMouseDown(row, event)
|
||||||
|
@ -1237,7 +1253,12 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRowMouseUp = (row: number, event: React.MouseEvent<any>) => {
|
private onRowMouseUp = (
|
||||||
|
indexPath: RowIndexPath,
|
||||||
|
event: React.MouseEvent<any>
|
||||||
|
) => {
|
||||||
|
const { row } = indexPath
|
||||||
|
|
||||||
if (!this.canSelectRow(row)) {
|
if (!this.canSelectRow(row)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1299,27 +1320,37 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRowClick = (row: number, event: React.MouseEvent<any>) => {
|
private onDropDataInsertion = (indexPath: RowIndexPath, data: DragData) => {
|
||||||
if (this.canSelectRow(row) && this.props.onRowClick) {
|
this.props.onDropDataInsertion?.(indexPath.row, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRowClick = (
|
||||||
|
indexPath: RowIndexPath,
|
||||||
|
event: React.MouseEvent<any>
|
||||||
|
) => {
|
||||||
|
if (this.canSelectRow(indexPath.row) && this.props.onRowClick) {
|
||||||
const rowCount = this.props.rowCount
|
const rowCount = this.props.rowCount
|
||||||
|
|
||||||
if (row < 0 || row >= rowCount) {
|
if (indexPath.row < 0 || indexPath.row >= rowCount) {
|
||||||
log.debug(
|
log.debug(
|
||||||
`[List.onRowClick] unable to onRowClick for row ${row} as it is outside the bounds of the array [0, ${rowCount}]`
|
`[List.onRowClick] unable to onRowClick for row ${indexPath.row} as it is outside the bounds of the array [0, ${rowCount}]`
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onRowClick(row, { kind: 'mouseclick', event })
|
this.props.onRowClick(indexPath.row, { kind: 'mouseclick', event })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRowDoubleClick = (row: number, event: React.MouseEvent<any>) => {
|
private onRowDoubleClick = (
|
||||||
|
indexPath: RowIndexPath,
|
||||||
|
event: React.MouseEvent<any>
|
||||||
|
) => {
|
||||||
if (!this.props.onRowDoubleClick) {
|
if (!this.props.onRowDoubleClick) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onRowDoubleClick(row, { kind: 'mouseclick', event })
|
this.props.onRowDoubleClick(indexPath.row, { kind: 'mouseclick', event })
|
||||||
}
|
}
|
||||||
|
|
||||||
private onScroll = ({
|
private onScroll = ({
|
||||||
|
|
191
app/src/ui/lib/list/section-list-selection.ts
Normal file
191
app/src/ui/lib/list/section-list-selection.ts
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import {
|
||||||
|
getTotalRowCount,
|
||||||
|
globalIndexToRowIndexPath,
|
||||||
|
InvalidRowIndexPath,
|
||||||
|
isValidRow,
|
||||||
|
RowIndexPath,
|
||||||
|
rowIndexPathEquals,
|
||||||
|
rowIndexPathToGlobalIndex,
|
||||||
|
} from './list-row-index-path'
|
||||||
|
|
||||||
|
export type SelectionDirection = 'up' | 'down'
|
||||||
|
|
||||||
|
interface ISelectRowAction {
|
||||||
|
/**
|
||||||
|
* The vertical direction use when searching for a selectable row.
|
||||||
|
*/
|
||||||
|
readonly direction: SelectionDirection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The starting row index to search from.
|
||||||
|
*/
|
||||||
|
readonly row: RowIndexPath
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag to indicate or not to look beyond the last or first
|
||||||
|
* row (depending on direction) such that given the last row and
|
||||||
|
* a downward direction will consider the first row as a
|
||||||
|
* candidate or given the first row and an upward direction
|
||||||
|
* will consider the last row as a candidate.
|
||||||
|
*
|
||||||
|
* Defaults to true if not set.
|
||||||
|
*/
|
||||||
|
readonly wrap?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface describing a user initiated selection change event
|
||||||
|
* originating from a pointer device clicking or pressing on an item.
|
||||||
|
*/
|
||||||
|
export interface IMouseClickSource {
|
||||||
|
readonly kind: 'mouseclick'
|
||||||
|
readonly event: React.MouseEvent<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface describing a user initiated selection change event
|
||||||
|
* originating from a pointer device hovering over an item.
|
||||||
|
* Only applicable when selectedOnHover is set.
|
||||||
|
*/
|
||||||
|
export interface IHoverSource {
|
||||||
|
readonly kind: 'hover'
|
||||||
|
readonly event: React.MouseEvent<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface describing a user initiated selection change event
|
||||||
|
* originating from a keyboard
|
||||||
|
*/
|
||||||
|
export interface IKeyboardSource {
|
||||||
|
readonly kind: 'keyboard'
|
||||||
|
readonly event: React.KeyboardEvent<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface describing a user initiated selection of all list
|
||||||
|
* items (usually by clicking the Edit > Select all menu item in
|
||||||
|
* the application window). This is highly specific to GitHub Desktop
|
||||||
|
*/
|
||||||
|
export interface ISelectAllSource {
|
||||||
|
readonly kind: 'select-all'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A type union of possible sources of a selection changed event */
|
||||||
|
export type SelectionSource =
|
||||||
|
| IMouseClickSource
|
||||||
|
| IHoverSource
|
||||||
|
| IKeyboardSource
|
||||||
|
| ISelectAllSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the next selectable row, given the direction and a starting
|
||||||
|
* row index. Whether a row is selectable or not is determined using
|
||||||
|
* the `canSelectRow` function, which defaults to true if not provided.
|
||||||
|
*
|
||||||
|
* Returns null if no row can be selected or if the only selectable row is
|
||||||
|
* identical to the given row parameter.
|
||||||
|
*/
|
||||||
|
export function findNextSelectableRow(
|
||||||
|
rowCount: ReadonlyArray<number>,
|
||||||
|
action: ISelectRowAction,
|
||||||
|
canSelectRow: (indexPath: RowIndexPath) => boolean = row => true
|
||||||
|
): RowIndexPath | null {
|
||||||
|
const totalRowCount = getTotalRowCount(rowCount)
|
||||||
|
if (totalRowCount === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { direction, row } = action
|
||||||
|
const wrap = action.wrap === undefined ? true : action.wrap
|
||||||
|
const rowIndex = rowIndexPathEquals(InvalidRowIndexPath, row)
|
||||||
|
? -1
|
||||||
|
: rowIndexPathToGlobalIndex(row, rowCount)
|
||||||
|
|
||||||
|
if (rowIndex === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the row value is in the range between 0 and rowCount - 1
|
||||||
|
//
|
||||||
|
// If the row falls outside this range, use the direction
|
||||||
|
// given to choose a suitable value:
|
||||||
|
//
|
||||||
|
// - move in an upward direction -> select last row
|
||||||
|
// - move in a downward direction -> select first row
|
||||||
|
//
|
||||||
|
let currentRow = isValidRow(row, rowCount)
|
||||||
|
? rowIndex
|
||||||
|
: direction === 'up'
|
||||||
|
? totalRowCount - 1
|
||||||
|
: 0
|
||||||
|
|
||||||
|
// handle specific case from switching from filter text to list
|
||||||
|
//
|
||||||
|
// locking currentRow to [0,rowCount) above means that the below loops
|
||||||
|
// will skip over the first entry
|
||||||
|
if (direction === 'down' && rowIndexPathEquals(row, InvalidRowIndexPath)) {
|
||||||
|
currentRow = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = direction === 'up' ? -1 : 1
|
||||||
|
|
||||||
|
// Iterate through all rows (starting offset from the
|
||||||
|
// given row and ending on and including the given row)
|
||||||
|
for (let i = 0; i < totalRowCount; i++) {
|
||||||
|
currentRow += delta
|
||||||
|
|
||||||
|
if (currentRow >= totalRowCount) {
|
||||||
|
// We've hit rock bottom, wrap around to the top
|
||||||
|
// if we're allowed to or give up.
|
||||||
|
if (wrap) {
|
||||||
|
currentRow = 0
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (currentRow < 0) {
|
||||||
|
// We've reached the top, wrap around to the bottom
|
||||||
|
// if we're allowed to or give up
|
||||||
|
if (wrap) {
|
||||||
|
currentRow = totalRowCount - 1
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentRowIndexPath = globalIndexToRowIndexPath(currentRow, rowCount)
|
||||||
|
if (
|
||||||
|
currentRowIndexPath !== null &&
|
||||||
|
!rowIndexPathEquals(row, currentRowIndexPath) &&
|
||||||
|
canSelectRow(currentRowIndexPath)
|
||||||
|
) {
|
||||||
|
return currentRowIndexPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the last selectable row in either direction, used
|
||||||
|
* for moving to the first or last selectable row in a list,
|
||||||
|
* i.e. Home/End key navigation.
|
||||||
|
*/
|
||||||
|
export function findLastSelectableRow(
|
||||||
|
direction: SelectionDirection,
|
||||||
|
rowCount: ReadonlyArray<number>,
|
||||||
|
canSelectRow: (indexPath: RowIndexPath) => boolean
|
||||||
|
): RowIndexPath | null {
|
||||||
|
const totalRowCount = getTotalRowCount(rowCount)
|
||||||
|
let i = direction === 'up' ? 0 : totalRowCount - 1
|
||||||
|
const delta = direction === 'up' ? 1 : -1
|
||||||
|
|
||||||
|
for (; i >= 0 && i < totalRowCount; i += delta) {
|
||||||
|
const indexPath = globalIndexToRowIndexPath(i, rowCount)
|
||||||
|
if (indexPath !== null && canSelectRow(indexPath)) {
|
||||||
|
return indexPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
1681
app/src/ui/lib/list/section-list.tsx
Normal file
1681
app/src/ui/lib/list/section-list.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
@ -87,7 +87,7 @@ export function findNextSelectableRow(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { direction, row } = action
|
const { direction, row } = action
|
||||||
const wrap = action.wrap === undefined ? true : action.wrap
|
const wrap = action.wrap ?? true
|
||||||
|
|
||||||
// Ensure the row value is in the range between 0 and rowCount - 1
|
// Ensure the row value is in the range between 0 and rowCount - 1
|
||||||
//
|
//
|
||||||
|
|
696
app/src/ui/lib/section-filter-list.tsx
Normal file
696
app/src/ui/lib/section-filter-list.tsx
Normal file
|
@ -0,0 +1,696 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
import { SectionList, ClickSource } from '../lib/list/section-list'
|
||||||
|
import {
|
||||||
|
findNextSelectableRow,
|
||||||
|
SelectionDirection,
|
||||||
|
} from '../lib/list/section-list-selection'
|
||||||
|
import { TextBox } from '../lib/text-box'
|
||||||
|
import { Row } from '../lib/row'
|
||||||
|
|
||||||
|
import { match, IMatch, IMatches } from '../../lib/fuzzy-find'
|
||||||
|
import { AriaLiveContainer } from '../accessibility/aria-live-container'
|
||||||
|
import {
|
||||||
|
InvalidRowIndexPath,
|
||||||
|
RowIndexPath,
|
||||||
|
rowIndexPathEquals,
|
||||||
|
} from './list/list-row-index-path'
|
||||||
|
import {
|
||||||
|
IFilterListGroup,
|
||||||
|
IFilterListItem,
|
||||||
|
SelectionSource,
|
||||||
|
} from './filter-list'
|
||||||
|
|
||||||
|
interface IFlattenedGroup {
|
||||||
|
readonly kind: 'group'
|
||||||
|
readonly identifier: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFlattenedItem<T extends IFilterListItem> {
|
||||||
|
readonly kind: 'item'
|
||||||
|
readonly item: T
|
||||||
|
/** Array of indexes in `item.text` that should be highlighted */
|
||||||
|
readonly matches: IMatches
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A row in the list. This is used internally after the user-provided groups are
|
||||||
|
* flattened.
|
||||||
|
*/
|
||||||
|
type IFilterListRow<T extends IFilterListItem> =
|
||||||
|
| IFlattenedGroup
|
||||||
|
| IFlattenedItem<T>
|
||||||
|
|
||||||
|
interface ISectionFilterListProps<T extends IFilterListItem> {
|
||||||
|
/** A class name for the wrapping element. */
|
||||||
|
readonly className?: string
|
||||||
|
|
||||||
|
/** The height of the rows. */
|
||||||
|
readonly rowHeight: number
|
||||||
|
|
||||||
|
/** The ordered groups to display in the list. */
|
||||||
|
readonly groups: ReadonlyArray<IFilterListGroup<T>>
|
||||||
|
|
||||||
|
/** The selected item. */
|
||||||
|
readonly selectedItem: T | null
|
||||||
|
|
||||||
|
/** Called to render each visible item. */
|
||||||
|
readonly renderItem: (item: T, matches: IMatches) => JSX.Element | null
|
||||||
|
|
||||||
|
/** Called to render header for the group with the given identifier. */
|
||||||
|
readonly renderGroupHeader?: (identifier: string) => JSX.Element | null
|
||||||
|
|
||||||
|
/** Called to render content before/above the filter and list. */
|
||||||
|
readonly renderPreList?: () => JSX.Element | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will be called when a pointer device is pressed and then
|
||||||
|
* released on a selectable row. Note that this follows the conventions
|
||||||
|
* of button elements such that pressing Enter or Space on a keyboard
|
||||||
|
* while focused on a particular row will also trigger this event. Consumers
|
||||||
|
* can differentiate between the two using the source parameter.
|
||||||
|
*
|
||||||
|
* Note that this event handler will not be called for keyboard events
|
||||||
|
* if `event.preventDefault()` was called in the onRowKeyDown event handler.
|
||||||
|
*
|
||||||
|
* Consumers of this event do _not_ have to call event.preventDefault,
|
||||||
|
* when this event is subscribed to the list will automatically call it.
|
||||||
|
*/
|
||||||
|
readonly onItemClick?: (item: T, source: ClickSource) => 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). This function
|
||||||
|
* will not be invoked when an already selected row is clicked on.
|
||||||
|
*
|
||||||
|
* @param selectedItem - The item 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: T | null,
|
||||||
|
source: SelectionSource
|
||||||
|
) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a key down happens in the filter text input. Users have a
|
||||||
|
* chance to respond or cancel the default behavior by calling
|
||||||
|
* `preventDefault()`.
|
||||||
|
*/
|
||||||
|
readonly onFilterKeyDown?: (
|
||||||
|
event: React.KeyboardEvent<HTMLInputElement>
|
||||||
|
) => void
|
||||||
|
|
||||||
|
/** Called when the Enter key is pressed in field of type search */
|
||||||
|
readonly onEnterPressedWithoutFilteredItems?: (text: string) => void
|
||||||
|
|
||||||
|
/** Aria label for a specific group */
|
||||||
|
readonly getGroupAriaLabel?: (group: number) => string | undefined
|
||||||
|
|
||||||
|
/** The current filter text to use in the form */
|
||||||
|
readonly filterText?: string
|
||||||
|
|
||||||
|
/** Called when the filter text is changed by the user */
|
||||||
|
readonly onFilterTextChanged?: (text: string) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the filter list should allow selection
|
||||||
|
* and filtering. Defaults to false.
|
||||||
|
*/
|
||||||
|
readonly disabled?: boolean
|
||||||
|
|
||||||
|
/** Any props which should cause a re-render if they change. */
|
||||||
|
readonly invalidationProps: any
|
||||||
|
|
||||||
|
/** Called to render content after the filter. */
|
||||||
|
readonly renderPostFilter?: () => JSX.Element | null
|
||||||
|
|
||||||
|
/** Called when there are no items to render. */
|
||||||
|
readonly renderNoItems?: () => JSX.Element | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reference to a TextBox that will be used to control this component.
|
||||||
|
*
|
||||||
|
* See https://github.com/desktop/desktop/issues/4317 for refactoring work to
|
||||||
|
* make this more composable which should make this unnecessary.
|
||||||
|
*/
|
||||||
|
readonly filterTextBox?: TextBox
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to fire when the items in the filter list are updated
|
||||||
|
*/
|
||||||
|
readonly onFilterListResultsChanged?: (resultCount: number) => void
|
||||||
|
|
||||||
|
/** Placeholder text for text box. Default is "Filter". */
|
||||||
|
readonly placeholderText?: string
|
||||||
|
|
||||||
|
/** If true, we do not render the filter. */
|
||||||
|
readonly hideFilterRow?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handler called whenever a context menu event is received on the
|
||||||
|
* row container element.
|
||||||
|
*
|
||||||
|
* The context menu is invoked when a user right clicks the row or
|
||||||
|
* uses keyboard shortcut.s
|
||||||
|
*/
|
||||||
|
readonly onItemContextMenu?: (
|
||||||
|
item: T,
|
||||||
|
event: React.MouseEvent<HTMLDivElement>
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFilterListState<T extends IFilterListItem> {
|
||||||
|
readonly rows: ReadonlyArray<ReadonlyArray<IFilterListRow<T>>>
|
||||||
|
readonly selectedRow: RowIndexPath
|
||||||
|
readonly filterValue: string
|
||||||
|
// Indices of groups in the filtered list
|
||||||
|
readonly groups: ReadonlyArray<number>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A List which includes the ability to filter based on its contents. */
|
||||||
|
export class SectionFilterList<
|
||||||
|
T extends IFilterListItem
|
||||||
|
> extends React.Component<ISectionFilterListProps<T>, IFilterListState<T>> {
|
||||||
|
private list: SectionList | null = null
|
||||||
|
private filterTextBox: TextBox | null = null
|
||||||
|
|
||||||
|
public constructor(props: ISectionFilterListProps<T>) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = createStateUpdate(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillMount() {
|
||||||
|
if (this.props.filterTextBox !== undefined) {
|
||||||
|
this.filterTextBox = this.props.filterTextBox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillReceiveProps(nextProps: ISectionFilterListProps<T>) {
|
||||||
|
this.setState(createStateUpdate(nextProps))
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(
|
||||||
|
prevProps: ISectionFilterListProps<T>,
|
||||||
|
prevState: IFilterListState<T>
|
||||||
|
) {
|
||||||
|
if (this.props.onSelectionChanged) {
|
||||||
|
const oldSelectedItemId = getItemIdFromRowIndex(
|
||||||
|
prevState.rows,
|
||||||
|
prevState.selectedRow
|
||||||
|
)
|
||||||
|
const newSelectedItemId = getItemIdFromRowIndex(
|
||||||
|
this.state.rows,
|
||||||
|
this.state.selectedRow
|
||||||
|
)
|
||||||
|
|
||||||
|
if (oldSelectedItemId !== newSelectedItemId) {
|
||||||
|
const propSelectionId = this.props.selectedItem
|
||||||
|
? this.props.selectedItem.id
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (propSelectionId !== newSelectedItemId) {
|
||||||
|
const newSelectedItem = getItemFromRowIndex(
|
||||||
|
this.state.rows,
|
||||||
|
this.state.selectedRow
|
||||||
|
)
|
||||||
|
this.props.onSelectionChanged(newSelectedItem, {
|
||||||
|
kind: 'filter',
|
||||||
|
filterText: this.props.filterText || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onFilterListResultsChanged !== undefined) {
|
||||||
|
const itemCount = this.state.rows
|
||||||
|
.flat()
|
||||||
|
.filter(row => row.kind === 'item').length
|
||||||
|
|
||||||
|
this.props.onFilterListResultsChanged(itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
if (this.filterTextBox !== null) {
|
||||||
|
this.filterTextBox.selectAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderTextBox() {
|
||||||
|
return (
|
||||||
|
<TextBox
|
||||||
|
ref={this.onTextBoxRef}
|
||||||
|
type="search"
|
||||||
|
autoFocus={true}
|
||||||
|
placeholder={this.props.placeholderText || 'Filter'}
|
||||||
|
className="filter-list-filter-field"
|
||||||
|
onValueChanged={this.onFilterValueChanged}
|
||||||
|
onEnterPressed={this.onEnterPressed}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
value={this.props.filterText}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderFilterRow() {
|
||||||
|
if (this.props.hideFilterRow === true) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row className="filter-field-row">
|
||||||
|
{this.props.filterTextBox === undefined ? this.renderTextBox() : null}
|
||||||
|
{this.props.renderPostFilter ? this.props.renderPostFilter() : null}
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const itemRows = this.state.rows.flat().filter(row => row.kind === 'item')
|
||||||
|
const resultsPluralized = itemRows.length === 1 ? 'result' : 'results'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classnames('filter-list', this.props.className)}>
|
||||||
|
<AriaLiveContainer trackedUserInput={this.state.filterValue}>
|
||||||
|
{itemRows.length} {resultsPluralized}
|
||||||
|
</AriaLiveContainer>
|
||||||
|
{this.props.renderPreList ? this.props.renderPreList() : null}
|
||||||
|
|
||||||
|
{this.renderFilterRow()}
|
||||||
|
|
||||||
|
<div className="filter-list-container">{this.renderContent()}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectNextItem(
|
||||||
|
focus: boolean = false,
|
||||||
|
inDirection: SelectionDirection = 'down'
|
||||||
|
) {
|
||||||
|
if (this.list === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let next: RowIndexPath | null = null
|
||||||
|
|
||||||
|
const rowCount = this.state.rows.map(r => r.length)
|
||||||
|
if (
|
||||||
|
this.state.selectedRow.row === -1 ||
|
||||||
|
this.state.selectedRow.row === this.state.rows.length
|
||||||
|
) {
|
||||||
|
next = findNextSelectableRow(
|
||||||
|
rowCount,
|
||||||
|
{
|
||||||
|
direction: inDirection,
|
||||||
|
row: InvalidRowIndexPath,
|
||||||
|
},
|
||||||
|
this.canSelectRow
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
next = findNextSelectableRow(
|
||||||
|
rowCount,
|
||||||
|
{
|
||||||
|
direction: inDirection,
|
||||||
|
row: this.state.selectedRow,
|
||||||
|
},
|
||||||
|
this.canSelectRow
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next !== null) {
|
||||||
|
this.setState({ selectedRow: next }, () => {
|
||||||
|
if (focus && this.list !== null) {
|
||||||
|
this.list.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderContent() {
|
||||||
|
if (this.state.rows.length === 0 && this.props.renderNoItems) {
|
||||||
|
return this.props.renderNoItems()
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<SectionList
|
||||||
|
ref={this.onListRef}
|
||||||
|
rowCount={this.state.rows.map(r => r.length)}
|
||||||
|
rowRenderer={this.renderRow}
|
||||||
|
sectionHasHeader={this.sectionHasHeader}
|
||||||
|
getSectionAriaLabel={this.getGroupAriaLabel}
|
||||||
|
rowHeight={this.props.rowHeight}
|
||||||
|
selectedRows={
|
||||||
|
rowIndexPathEquals(this.state.selectedRow, InvalidRowIndexPath)
|
||||||
|
? []
|
||||||
|
: [this.state.selectedRow]
|
||||||
|
}
|
||||||
|
onSelectedRowChanged={this.onSelectedRowChanged}
|
||||||
|
onRowClick={this.onRowClick}
|
||||||
|
onRowKeyDown={this.onRowKeyDown}
|
||||||
|
onRowContextMenu={this.onRowContextMenu}
|
||||||
|
canSelectRow={this.canSelectRow}
|
||||||
|
invalidationProps={{
|
||||||
|
...this.props,
|
||||||
|
...this.props.invalidationProps,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sectionHasHeader = (section: number) => {
|
||||||
|
const rows = this.state.rows[section]
|
||||||
|
return rows.length > 0 && rows[0].kind === 'group'
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGroupAriaLabel = (group: number) => {
|
||||||
|
return this.props.getGroupAriaLabel?.(this.state.groups[group])
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderRow = (index: RowIndexPath) => {
|
||||||
|
const row = this.state.rows[index.section][index.row]
|
||||||
|
if (row.kind === 'item') {
|
||||||
|
return this.props.renderItem(row.item, row.matches)
|
||||||
|
} else if (this.props.renderGroupHeader) {
|
||||||
|
return this.props.renderGroupHeader(row.identifier)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTextBoxRef = (component: TextBox | null) => {
|
||||||
|
this.filterTextBox = component
|
||||||
|
}
|
||||||
|
|
||||||
|
private onListRef = (instance: SectionList | null) => {
|
||||||
|
this.list = instance
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFilterValueChanged = (text: string) => {
|
||||||
|
if (this.props.onFilterTextChanged) {
|
||||||
|
this.props.onFilterTextChanged(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEnterPressed = (text: string) => {
|
||||||
|
const rows = this.state.rows.length
|
||||||
|
if (
|
||||||
|
rows === 0 &&
|
||||||
|
text.trim().length > 0 &&
|
||||||
|
this.props.onEnterPressedWithoutFilteredItems !== undefined
|
||||||
|
) {
|
||||||
|
this.props.onEnterPressedWithoutFilteredItems(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSelectedRowChanged = (
|
||||||
|
index: RowIndexPath,
|
||||||
|
source: SelectionSource
|
||||||
|
) => {
|
||||||
|
this.setState({ selectedRow: index })
|
||||||
|
|
||||||
|
if (this.props.onSelectionChanged) {
|
||||||
|
const row = this.state.rows[index.section][index.row]
|
||||||
|
if (row.kind === 'item') {
|
||||||
|
this.props.onSelectionChanged(row.item, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private canSelectRow = (index: RowIndexPath) => {
|
||||||
|
if (this.props.disabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = this.state.rows[index.section][index.row]
|
||||||
|
return row.kind === 'item'
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRowClick = (index: RowIndexPath, source: ClickSource) => {
|
||||||
|
if (this.props.onItemClick) {
|
||||||
|
const row = this.state.rows[index.section][index.row]
|
||||||
|
|
||||||
|
if (row.kind === 'item') {
|
||||||
|
this.props.onItemClick(row.item, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRowContextMenu = (
|
||||||
|
index: RowIndexPath,
|
||||||
|
source: React.MouseEvent<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
if (!this.props.onItemContextMenu) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = this.state.rows[index.section][index.row]
|
||||||
|
|
||||||
|
if (row.kind !== 'item') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onItemContextMenu(row.item, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRowKeyDown = (
|
||||||
|
indexPath: RowIndexPath,
|
||||||
|
event: React.KeyboardEvent<any>
|
||||||
|
) => {
|
||||||
|
const list = this.list
|
||||||
|
if (!list) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowCount = this.state.rows.map(r => r.length)
|
||||||
|
|
||||||
|
const firstSelectableRow = findNextSelectableRow(
|
||||||
|
rowCount,
|
||||||
|
{ direction: 'down', row: InvalidRowIndexPath },
|
||||||
|
this.canSelectRow
|
||||||
|
)
|
||||||
|
const lastSelectableRow = findNextSelectableRow(
|
||||||
|
rowCount,
|
||||||
|
{
|
||||||
|
direction: 'up',
|
||||||
|
row: {
|
||||||
|
section: 0,
|
||||||
|
row: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.canSelectRow
|
||||||
|
)
|
||||||
|
|
||||||
|
let shouldFocus = false
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.key === 'ArrowUp' &&
|
||||||
|
firstSelectableRow &&
|
||||||
|
rowIndexPathEquals(indexPath, firstSelectableRow)
|
||||||
|
) {
|
||||||
|
shouldFocus = true
|
||||||
|
} else if (
|
||||||
|
event.key === 'ArrowDown' &&
|
||||||
|
lastSelectableRow &&
|
||||||
|
rowIndexPathEquals(indexPath, lastSelectableRow)
|
||||||
|
) {
|
||||||
|
shouldFocus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldFocus) {
|
||||||
|
const textBox = this.filterTextBox
|
||||||
|
|
||||||
|
if (textBox) {
|
||||||
|
event.preventDefault()
|
||||||
|
textBox.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
const list = this.list
|
||||||
|
const key = event.key
|
||||||
|
|
||||||
|
if (!list) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onFilterKeyDown) {
|
||||||
|
this.props.onFilterKeyDown(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.defaultPrevented) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowCount = this.state.rows.map(r => r.length)
|
||||||
|
|
||||||
|
if (key === 'ArrowDown') {
|
||||||
|
if (rowCount.length > 0) {
|
||||||
|
const selectedRow = findNextSelectableRow(
|
||||||
|
rowCount,
|
||||||
|
{ direction: 'down', row: InvalidRowIndexPath },
|
||||||
|
this.canSelectRow
|
||||||
|
)
|
||||||
|
if (selectedRow != null) {
|
||||||
|
this.setState({ selectedRow }, () => {
|
||||||
|
list.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
} else if (key === 'ArrowUp') {
|
||||||
|
if (rowCount.length > 0) {
|
||||||
|
const selectedRow = findNextSelectableRow(
|
||||||
|
rowCount,
|
||||||
|
{
|
||||||
|
direction: 'up',
|
||||||
|
row: {
|
||||||
|
section: 0,
|
||||||
|
row: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.canSelectRow
|
||||||
|
)
|
||||||
|
if (selectedRow != null) {
|
||||||
|
this.setState({ selectedRow }, () => {
|
||||||
|
list.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
} else if (key === 'Enter') {
|
||||||
|
// no repositories currently displayed, bail out
|
||||||
|
if (rowCount.length === 0) {
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterText = this.props.filterText
|
||||||
|
|
||||||
|
if (filterText !== undefined && !/\S/.test(filterText)) {
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = findNextSelectableRow(
|
||||||
|
rowCount,
|
||||||
|
{ direction: 'down', row: InvalidRowIndexPath },
|
||||||
|
this.canSelectRow
|
||||||
|
)
|
||||||
|
|
||||||
|
if (row != null) {
|
||||||
|
this.onRowClick(row, { kind: 'keyboard', event })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getText<T extends IFilterListItem>(
|
||||||
|
item: T
|
||||||
|
): ReadonlyArray<string> {
|
||||||
|
return item['text']
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFirstVisibleRow<T extends IFilterListItem>(
|
||||||
|
rows: ReadonlyArray<ReadonlyArray<IFilterListRow<T>>>
|
||||||
|
): RowIndexPath {
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const groupRows = rows[i]
|
||||||
|
for (let j = 0; j < groupRows.length; j++) {
|
||||||
|
const row = groupRows[j]
|
||||||
|
if (row.kind === 'item') {
|
||||||
|
return { section: i, row: j }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return InvalidRowIndexPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStateUpdate<T extends IFilterListItem>(
|
||||||
|
props: ISectionFilterListProps<T>
|
||||||
|
) {
|
||||||
|
const rows = new Array<Array<IFilterListRow<T>>>()
|
||||||
|
const filter = (props.filterText || '').toLowerCase()
|
||||||
|
let selectedRow = InvalidRowIndexPath
|
||||||
|
let section = 0
|
||||||
|
const selectedItem = props.selectedItem
|
||||||
|
const groupIndices = []
|
||||||
|
|
||||||
|
for (const [idx, group] of props.groups.entries()) {
|
||||||
|
const groupRows = new Array<IFilterListRow<T>>()
|
||||||
|
const items: ReadonlyArray<IMatch<T>> = filter
|
||||||
|
? match(filter, group.items, getText)
|
||||||
|
: group.items.map(item => ({
|
||||||
|
score: 1,
|
||||||
|
matches: { title: [], subtitle: [] },
|
||||||
|
item,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndices.push(idx)
|
||||||
|
|
||||||
|
if (props.renderGroupHeader) {
|
||||||
|
groupRows.push({ kind: 'group', identifier: group.identifier })
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { item, matches } of items) {
|
||||||
|
if (selectedItem && item.id === selectedItem.id) {
|
||||||
|
selectedRow = {
|
||||||
|
section,
|
||||||
|
row: groupRows.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groupRows.push({ kind: 'item', item, matches })
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(groupRows)
|
||||||
|
section++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedRow.row < 0 && filter.length) {
|
||||||
|
// If the selected item isn't in the list (e.g., filtered out), then
|
||||||
|
// select the first visible item.
|
||||||
|
selectedRow = getFirstVisibleRow(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rows: rows, selectedRow, filterValue: filter, groups: groupIndices }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemFromRowIndex<T extends IFilterListItem>(
|
||||||
|
items: ReadonlyArray<ReadonlyArray<IFilterListRow<T>>>,
|
||||||
|
index: RowIndexPath
|
||||||
|
): T | null {
|
||||||
|
if (index.section < 0 || index.section >= items.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = items[index.section]
|
||||||
|
if (index.row < 0 || index.row >= group.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = group[index.row]
|
||||||
|
|
||||||
|
if (row.kind === 'item') {
|
||||||
|
return row.item
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemIdFromRowIndex<T extends IFilterListItem>(
|
||||||
|
items: ReadonlyArray<ReadonlyArray<IFilterListRow<T>>>,
|
||||||
|
index: RowIndexPath
|
||||||
|
): string | null {
|
||||||
|
const item = getItemFromRowIndex(items, index)
|
||||||
|
return item ? item.id : null
|
||||||
|
}
|
|
@ -24,6 +24,8 @@ import { TooltippedContent } from '../lib/tooltipped-content'
|
||||||
import memoizeOne from 'memoize-one'
|
import memoizeOne from 'memoize-one'
|
||||||
import { KeyboardShortcut } from '../keyboard-shortcut/keyboard-shortcut'
|
import { KeyboardShortcut } from '../keyboard-shortcut/keyboard-shortcut'
|
||||||
import { generateRepositoryListContextMenu } from '../repositories-list/repository-list-item-context-menu'
|
import { generateRepositoryListContextMenu } from '../repositories-list/repository-list-item-context-menu'
|
||||||
|
import { SectionFilterList } from '../lib/section-filter-list'
|
||||||
|
import { enableSectionList } from '../../lib/feature-flag'
|
||||||
|
|
||||||
const BlankSlateImage = encodePathAsUrl(__dirname, 'static/empty-no-repo.svg')
|
const BlankSlateImage = encodePathAsUrl(__dirname, 'static/empty-no-repo.svg')
|
||||||
|
|
||||||
|
@ -241,25 +243,31 @@ export class RepositoriesList extends React.Component<
|
||||||
]
|
]
|
||||||
: baseGroups
|
: baseGroups
|
||||||
|
|
||||||
|
const getGroupAriaLabel = (group: number) => groups[group].identifier
|
||||||
|
|
||||||
|
const ListComponent = enableSectionList() ? SectionFilterList : FilterList
|
||||||
|
const filterListProps: typeof ListComponent['prototype']['props'] = {
|
||||||
|
rowHeight: RowHeight,
|
||||||
|
selectedItem: selectedItem,
|
||||||
|
filterText: this.props.filterText,
|
||||||
|
onFilterTextChanged: this.props.onFilterTextChanged,
|
||||||
|
renderItem: this.renderItem,
|
||||||
|
renderGroupHeader: this.renderGroupHeader,
|
||||||
|
onItemClick: this.onItemClick,
|
||||||
|
renderPostFilter: this.renderPostFilter,
|
||||||
|
renderNoItems: this.renderNoItems,
|
||||||
|
groups: groups,
|
||||||
|
invalidationProps: {
|
||||||
|
repositories: this.props.repositories,
|
||||||
|
filterText: this.props.filterText,
|
||||||
|
},
|
||||||
|
onItemContextMenu: this.onItemContextMenu,
|
||||||
|
getGroupAriaLabel,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="repository-list">
|
<div className="repository-list">
|
||||||
<FilterList<IRepositoryListItem>
|
<ListComponent {...filterListProps} />
|
||||||
rowHeight={RowHeight}
|
|
||||||
selectedItem={selectedItem}
|
|
||||||
filterText={this.props.filterText}
|
|
||||||
onFilterTextChanged={this.props.onFilterTextChanged}
|
|
||||||
renderItem={this.renderItem}
|
|
||||||
renderGroupHeader={this.renderGroupHeader}
|
|
||||||
onItemClick={this.onItemClick}
|
|
||||||
renderPostFilter={this.renderPostFilter}
|
|
||||||
renderNoItems={this.renderNoItems}
|
|
||||||
groups={groups}
|
|
||||||
invalidationProps={{
|
|
||||||
repositories: this.props.repositories,
|
|
||||||
filterText: this.props.filterText,
|
|
||||||
}}
|
|
||||||
onItemContextMenu={this.onItemContextMenu}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ import {
|
||||||
import { DialogHeader } from '../dialog/header'
|
import { DialogHeader } from '../dialog/header'
|
||||||
import { Dispatcher } from '../dispatcher'
|
import { Dispatcher } from '../dispatcher'
|
||||||
import { Button } from '../lib/button'
|
import { Button } from '../lib/button'
|
||||||
import { List } from '../lib/list'
|
import { RowIndexPath } from '../lib/list/list-row-index-path'
|
||||||
|
import { SectionList } from '../lib/list/section-list'
|
||||||
import { Loading } from '../lib/loading'
|
import { Loading } from '../lib/loading'
|
||||||
import { getPullRequestReviewStateIcon } from '../notifications/pull-request-review-helpers'
|
import { getPullRequestReviewStateIcon } from '../notifications/pull-request-review-helpers'
|
||||||
import { Octicon } from '../octicons'
|
import { Octicon } from '../octicons'
|
||||||
|
@ -397,9 +398,9 @@ export class TestNotifications extends React.Component<
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Pull requests:
|
Pull requests:
|
||||||
<List
|
<SectionList
|
||||||
rowHeight={40}
|
rowHeight={40}
|
||||||
rowCount={pullRequests.length}
|
rowCount={[pullRequests.length]}
|
||||||
rowRenderer={this.renderPullRequestRow}
|
rowRenderer={this.renderPullRequestRow}
|
||||||
selectedRows={[]}
|
selectedRows={[]}
|
||||||
onRowClick={this.onPullRequestRowClick}
|
onRowClick={this.onPullRequestRowClick}
|
||||||
|
@ -408,8 +409,8 @@ export class TestNotifications extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPullRequestRowClick = (row: number) => {
|
private onPullRequestRowClick = (indexPath: RowIndexPath) => {
|
||||||
const pullRequest = this.state.pullRequests[row]
|
const pullRequest = this.state.pullRequests[indexPath.row]
|
||||||
const stepResults = this.state.stepResults
|
const stepResults = this.state.stepResults
|
||||||
stepResults.set(TestNotificationStepKind.SelectPullRequest, {
|
stepResults.set(TestNotificationStepKind.SelectPullRequest, {
|
||||||
kind: TestNotificationStepKind.SelectPullRequest,
|
kind: TestNotificationStepKind.SelectPullRequest,
|
||||||
|
@ -440,9 +441,9 @@ export class TestNotifications extends React.Component<
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Reviews:
|
Reviews:
|
||||||
<List
|
<SectionList
|
||||||
rowHeight={40}
|
rowHeight={40}
|
||||||
rowCount={reviews.length}
|
rowCount={[reviews.length]}
|
||||||
rowRenderer={this.renderPullRequestReviewRow}
|
rowRenderer={this.renderPullRequestReviewRow}
|
||||||
selectedRows={[]}
|
selectedRows={[]}
|
||||||
onRowClick={this.onPullRequestReviewRowClick}
|
onRowClick={this.onPullRequestReviewRowClick}
|
||||||
|
@ -451,8 +452,8 @@ export class TestNotifications extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPullRequestReviewRowClick = (row: number) => {
|
private onPullRequestReviewRowClick = (indexPath: RowIndexPath) => {
|
||||||
const review = this.state.reviews[row]
|
const review = this.state.reviews[indexPath.row]
|
||||||
const stepResults = this.state.stepResults
|
const stepResults = this.state.stepResults
|
||||||
stepResults.set(TestNotificationStepKind.SelectPullRequestReview, {
|
stepResults.set(TestNotificationStepKind.SelectPullRequestReview, {
|
||||||
kind: TestNotificationStepKind.SelectPullRequestReview,
|
kind: TestNotificationStepKind.SelectPullRequestReview,
|
||||||
|
@ -483,9 +484,9 @@ export class TestNotifications extends React.Component<
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Comments:
|
Comments:
|
||||||
<List
|
<SectionList
|
||||||
rowHeight={40}
|
rowHeight={40}
|
||||||
rowCount={comments.length}
|
rowCount={[comments.length]}
|
||||||
rowRenderer={this.renderPullRequestCommentRow}
|
rowRenderer={this.renderPullRequestCommentRow}
|
||||||
selectedRows={[]}
|
selectedRows={[]}
|
||||||
onRowClick={this.onPullRequestCommentRowClick}
|
onRowClick={this.onPullRequestCommentRowClick}
|
||||||
|
@ -494,8 +495,8 @@ export class TestNotifications extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPullRequestCommentRowClick = (row: number) => {
|
private onPullRequestCommentRowClick = (indexPath: RowIndexPath) => {
|
||||||
const comment = this.state.comments[row]
|
const comment = this.state.comments[indexPath.row]
|
||||||
const stepResults = this.state.stepResults
|
const stepResults = this.state.stepResults
|
||||||
stepResults.set(TestNotificationStepKind.SelectPullRequestComment, {
|
stepResults.set(TestNotificationStepKind.SelectPullRequestComment, {
|
||||||
kind: TestNotificationStepKind.SelectPullRequestComment,
|
kind: TestNotificationStepKind.SelectPullRequestComment,
|
||||||
|
@ -513,8 +514,8 @@ export class TestNotifications extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPullRequestCommentRow = (row: number) => {
|
private renderPullRequestCommentRow = (indexPath: RowIndexPath) => {
|
||||||
const comment = this.state.comments[row]
|
const comment = this.state.comments[indexPath.row]
|
||||||
return (
|
return (
|
||||||
<TestNotificationItemRowContent
|
<TestNotificationItemRowContent
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
|
@ -528,8 +529,8 @@ export class TestNotifications extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPullRequestReviewRow = (row: number) => {
|
private renderPullRequestReviewRow = (indexPath: RowIndexPath) => {
|
||||||
const review = this.state.reviews[row]
|
const review = this.state.reviews[indexPath.row]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TestNotificationItemRowContent
|
<TestNotificationItemRowContent
|
||||||
|
@ -555,8 +556,8 @@ export class TestNotifications extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPullRequestRow = (row: number) => {
|
private renderPullRequestRow = (indexPath: RowIndexPath) => {
|
||||||
const pullRequest = this.state.pullRequests[row]
|
const pullRequest = this.state.pullRequests[indexPath.row]
|
||||||
const repository = this.props.repository.gitHubRepository
|
const repository = this.props.repository.gitHubRepository
|
||||||
const endpointHtmlUrl = getHTMLURL(repository.endpoint)
|
const endpointHtmlUrl = getHTMLURL(repository.endpoint)
|
||||||
const htmlURL = `${endpointHtmlUrl}/${repository.owner.login}/${repository.name}/pull/${pullRequest.pullRequestNumber}`
|
const htmlURL = `${endpointHtmlUrl}/${repository.owner.login}/${repository.name}/pull/${pullRequest.pullRequestNumber}`
|
||||||
|
|
88
app/test/unit/section-list-selection-test.ts
Normal file
88
app/test/unit/section-list-selection-test.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import {
|
||||||
|
InvalidRowIndexPath,
|
||||||
|
rowIndexPathEquals,
|
||||||
|
} from '../../src/ui/lib/list/list-row-index-path'
|
||||||
|
import { findNextSelectableRow } from '../../src/ui/lib/list/section-list-selection'
|
||||||
|
|
||||||
|
describe('section-list-selection', () => {
|
||||||
|
describe('findNextSelectableRow', () => {
|
||||||
|
const rowCount = [5, 3, 8]
|
||||||
|
|
||||||
|
it('returns first row when selecting down outside list (filter text)', () => {
|
||||||
|
const selectedRow = findNextSelectableRow(rowCount, {
|
||||||
|
direction: 'down',
|
||||||
|
row: InvalidRowIndexPath,
|
||||||
|
})
|
||||||
|
expect(selectedRow?.row).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns first selectable row when header is first', () => {
|
||||||
|
const selectedRow = findNextSelectableRow(
|
||||||
|
rowCount,
|
||||||
|
{
|
||||||
|
direction: 'down',
|
||||||
|
row: InvalidRowIndexPath,
|
||||||
|
},
|
||||||
|
row => {
|
||||||
|
if (row.section === 0 && row.row === 0) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(selectedRow?.row).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns first row when selecting down from last row', () => {
|
||||||
|
const lastRow = rowCount[0] - 1
|
||||||
|
const selectedRow = findNextSelectableRow(rowCount, {
|
||||||
|
direction: 'down',
|
||||||
|
row: {
|
||||||
|
section: 0,
|
||||||
|
row: lastRow,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(selectedRow?.row).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns last row when selecting up from top row', () => {
|
||||||
|
const selectedRow = findNextSelectableRow(rowCount, {
|
||||||
|
direction: 'up',
|
||||||
|
row: {
|
||||||
|
section: 0,
|
||||||
|
row: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
rowIndexPathEquals(selectedRow!, { section: 2, row: 7 })
|
||||||
|
).toBeTrue()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns first row of next section when selecting down from last row of a section', () => {
|
||||||
|
const selectedRow = findNextSelectableRow(rowCount, {
|
||||||
|
direction: 'down',
|
||||||
|
row: {
|
||||||
|
section: 0,
|
||||||
|
row: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
rowIndexPathEquals(selectedRow!, { section: 1, row: 0 })
|
||||||
|
).toBeTrue()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns last row of previous section when selecting up from first row of a section', () => {
|
||||||
|
const selectedRow = findNextSelectableRow(rowCount, {
|
||||||
|
direction: 'up',
|
||||||
|
row: {
|
||||||
|
section: 2,
|
||||||
|
row: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
rowIndexPathEquals(selectedRow!, { section: 1, row: 2 })
|
||||||
|
).toBeTrue()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -43,23 +43,3 @@ $ yarn run package
|
||||||
If you think you've found a solution, please submit a pull request to [`shiftkey/desktop`](https://github.com/shiftkey/desktop) explaining the change and what it fixes. If you're not quite sure, open an issue on the [`shiftkey/desktop`](https://github.com/shiftkey/desktop) fork explaining what you've found and where you think the problem lies. Maybe someone else has insight into the issue.
|
If you think you've found a solution, please submit a pull request to [`shiftkey/desktop`](https://github.com/shiftkey/desktop) explaining the change and what it fixes. If you're not quite sure, open an issue on the [`shiftkey/desktop`](https://github.com/shiftkey/desktop) fork explaining what you've found and where you think the problem lies. Maybe someone else has insight into the issue.
|
||||||
|
|
||||||
[**@shiftkey**](https://github.com/shiftkey) will co-ordinate upstreaming merged pull requests to the main repository.
|
[**@shiftkey**](https://github.com/shiftkey) will co-ordinate upstreaming merged pull requests to the main repository.
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
We use `electron-packager` to generate the artifacts and `electron-builder` to generate the installer.
|
|
||||||
|
|
||||||
`electron-packager` details:
|
|
||||||
|
|
||||||
* [API options](https://github.com/electron-userland/electron-packager/blob/development/docs/api.md#options)
|
|
||||||
* [`dist-info.js` config file](https://github.com/desktop/desktop/blob/development/script/dist-info.js)
|
|
||||||
* [Usage in Desktop](https://github.com/desktop/desktop/blob/development/script/build.ts#L98-L151)
|
|
||||||
|
|
||||||
`dist-info.js` contains the various metadata we provide to Desktop as part of packaging. This seems fairly stable, but we might need to tweak some things in here for Linux-specific changes.
|
|
||||||
|
|
||||||
`electron-builder` details:
|
|
||||||
|
|
||||||
* [API options](https://www.electron.build/configuration/linux)
|
|
||||||
* [`electron-builder-linux.yml` config file](https://github.com/desktop/desktop/blob/development/script/electron-builder-linux.yml)
|
|
||||||
* [Usage in Desktop](https://github.com/desktop/desktop/blob/development/script/package.ts#L124-L145)
|
|
||||||
|
|
||||||
We use `electron-builder-linux.yml` to configure the installers, so please investigate the documentation if you find a problem with an installer to see if something has been overlooked and can be fixed fairly easily.
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ In the interest of stability and caution we tend to stay a version (or more) beh
|
||||||
| Dependency | Versions Behind Latest |
|
| Dependency | Versions Behind Latest |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| electron | >= 1 major |
|
| electron | >= 1 major |
|
||||||
| electron-builder | >= 1 minor |
|
|
||||||
| electron-packager | >= 1 major |
|
| electron-packager | >= 1 major |
|
||||||
| electron-winstaller | >= 1 minor |
|
| electron-winstaller | >= 1 minor |
|
||||||
| typescript | >= 1 minor |
|
| typescript | >= 1 minor |
|
||||||
|
@ -76,7 +75,6 @@ These are the most important dependencies to the app, and include:
|
||||||
- `package.json`
|
- `package.json`
|
||||||
- `@types/node`
|
- `@types/node`
|
||||||
- `electron`
|
- `electron`
|
||||||
- `electron-builder`
|
|
||||||
- `electron-packager`
|
- `electron-packager`
|
||||||
- `electron-winstaller`
|
- `electron-winstaller`
|
||||||
- `typescript`
|
- `typescript`
|
||||||
|
|
|
@ -107,16 +107,8 @@ Other things to note about the Windows packaging process:
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
Desktop uses `electron-builder` to generate these three packages:
|
Refer to the [`shiftkey/desktop`](https://github.com/shiftkey/desktop) fork
|
||||||
|
for packaging details about Linux.
|
||||||
- `.deb` package for Debian-based distributions
|
|
||||||
- `.rpm` package for RPM-based various distributions
|
|
||||||
- `.AppImage` package for various distributions (no elevated permissions
|
|
||||||
required)
|
|
||||||
- `.snap` package for various distributions
|
|
||||||
|
|
||||||
The `script/electron-builder-linux.yml` configuration file contains the details
|
|
||||||
applied to each package (if applicable).
|
|
||||||
|
|
||||||
## `script/publish.ts`
|
## `script/publish.ts`
|
||||||
|
|
||||||
|
|
|
@ -155,7 +155,6 @@
|
||||||
"@types/webpack-merge": "^5.0.0",
|
"@types/webpack-merge": "^5.0.0",
|
||||||
"@types/xml2js": "^0.4.11",
|
"@types/xml2js": "^0.4.11",
|
||||||
"electron": "24.4.0",
|
"electron": "24.4.0",
|
||||||
"electron-builder": "^23.6.0",
|
|
||||||
"electron-packager": "^17.1.1",
|
"electron-packager": "^17.1.1",
|
||||||
"electron-winstaller": "^5.0.0",
|
"electron-winstaller": "^5.0.0",
|
||||||
"eslint-plugin-github": "^4.3.7",
|
"eslint-plugin-github": "^4.3.7",
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
productName: 'GitHubDesktop'
|
|
||||||
artifactName: '${productName}-${os}-${arch}-${version}.${ext}'
|
|
||||||
linux:
|
|
||||||
category: 'GNOME;GTK;Development'
|
|
||||||
packageCategory: 'GNOME;GTK;Development'
|
|
||||||
icon: 'app/static/logos'
|
|
||||||
target:
|
|
||||||
- deb
|
|
||||||
- rpm
|
|
||||||
- AppImage
|
|
||||||
maintainer: 'GitHub, Inc <opensource+desktop@github.com>'
|
|
||||||
deb:
|
|
||||||
afterInstall: './script/linux-after-install.sh'
|
|
||||||
afterRemove: './script/linux-after-remove.sh'
|
|
||||||
depends:
|
|
||||||
# default Electron dependencies
|
|
||||||
- gconf2
|
|
||||||
- gconf-service
|
|
||||||
- libnotify4
|
|
||||||
- libappindicator1
|
|
||||||
- libxtst6
|
|
||||||
- libnss3
|
|
||||||
# dugite-native dependencies
|
|
||||||
- libcurl3 | libcurl4
|
|
||||||
# keytar dependencies
|
|
||||||
- libsecret-1-0
|
|
||||||
rpm:
|
|
||||||
depends:
|
|
||||||
# default Electron dependencies
|
|
||||||
- libXScrnSaver
|
|
||||||
- libappindicator
|
|
||||||
- libnotify
|
|
||||||
# dugite-native dependencies
|
|
||||||
- libcurl
|
|
||||||
# keytar dependencies
|
|
||||||
- libsecret
|
|
|
@ -1,25 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
PROFILE_D_FILE="/etc/profile.d/github-desktop.sh"
|
|
||||||
INSTALL_DIR="/opt/${productFilename}"
|
|
||||||
SCRIPT=$"#!/bin/sh
|
|
||||||
export PATH=\"$INSTALL_DIR:\$PATH\""
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
configure)
|
|
||||||
echo "$SCRIPT" > "${PROFILE_D_FILE}";
|
|
||||||
. "${PROFILE_D_FILE}";
|
|
||||||
;;
|
|
||||||
|
|
||||||
abort-upgrade|abort-remove|abort-deconfigure)
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "postinst called with unknown argument \`$1'" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
PROFILE_D_FILE="/etc/profile.d/github-desktop.sh"
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
|
||||||
echo "#!/bin/sh" > "${PROFILE_D_FILE}";
|
|
||||||
. "${PROFILE_D_FILE}";
|
|
||||||
rm "${PROFILE_D_FILE}";
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "postrm called with unknown argument \`$1'" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -32,10 +32,8 @@ if (process.platform === 'darwin') {
|
||||||
packageOSX()
|
packageOSX()
|
||||||
} else if (process.platform === 'win32') {
|
} else if (process.platform === 'win32') {
|
||||||
packageWindows()
|
packageWindows()
|
||||||
} else if (process.platform === 'linux') {
|
|
||||||
packageLinux()
|
|
||||||
} else {
|
} else {
|
||||||
console.error(`I dunno how to package for ${process.platform} :(`)
|
console.error(`I don't know how to package for ${process.platform} :(`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,27 +136,3 @@ function packageWindows() {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function packageLinux() {
|
|
||||||
const electronBuilder = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
'..',
|
|
||||||
'node_modules',
|
|
||||||
'.bin',
|
|
||||||
'electron-builder'
|
|
||||||
)
|
|
||||||
|
|
||||||
const configPath = path.resolve(__dirname, 'electron-builder-linux.yml')
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
'build',
|
|
||||||
'--prepackaged',
|
|
||||||
distPath,
|
|
||||||
'--x64',
|
|
||||||
'--config',
|
|
||||||
configPath,
|
|
||||||
]
|
|
||||||
|
|
||||||
console.log('Packaging for Linux…')
|
|
||||||
cp.spawnSync(electronBuilder, args, { stdio: 'inherit' })
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue