mirror of
https://github.com/desktop/desktop
synced 2024-10-05 23:59:33 +00:00
Merge branch 'development' into releases/3.2.4
This commit is contained in:
commit
5d8f7c7b72
|
@ -186,6 +186,21 @@ rules:
|
|||
- selector: ExportDefaultDeclaration
|
||||
message: Use of default exports is forbidden
|
||||
|
||||
###########
|
||||
# jsx-a11y #
|
||||
###########
|
||||
|
||||
# autofocus is fine when it is being used to set focus to something in a way
|
||||
# that doesn't skip any context or inputs. For example, in a named dialog, if you had
|
||||
# a close button, a heading reflecting the name, and a labelled text input,
|
||||
# focusing the text input wouldn't disadvantage a user because they're not
|
||||
# missing anything of importance before it. The problem is when it is used on
|
||||
# larger web pages, e.g. to focus a form field in the main content, entirely
|
||||
# skipping the header and often much else besides.
|
||||
jsx-a11y/no-autofocus:
|
||||
- warn
|
||||
- ignoreNonDOM: true
|
||||
|
||||
overrides:
|
||||
- files: '*.d.ts'
|
||||
rules:
|
||||
|
|
2
.github/workflows/release-pr.yml
vendored
2
.github/workflows/release-pr.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
|||
private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: peter-evans/create-pull-request@v5.0.0
|
||||
uses: peter-evans/create-pull-request@v5.0.1
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test')
|
||||
with:
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@github/alive-client": "^0.0.2",
|
||||
"app-path": "^3.3.0",
|
||||
"byline": "^5.0.0",
|
||||
|
|
|
@ -27,15 +27,15 @@ interface IGitRemoteURL {
|
|||
const remoteRegexes: ReadonlyArray<{ protocol: GitProtocol; regex: RegExp }> = [
|
||||
{
|
||||
protocol: 'https',
|
||||
regex: new RegExp('^https?://(?:.+@)?(.+)/(.+)/(.+?)(?:/|.git/?)?$'),
|
||||
regex: new RegExp('^https?://(?:.+@)?(.+)/([^/]+)/([^/]+?)(?:/|.git/?)?$'),
|
||||
},
|
||||
{
|
||||
protocol: 'ssh',
|
||||
regex: new RegExp('^git@(.+):(.+)/(.+?)(?:/|.git)?$'),
|
||||
regex: new RegExp('^git@(.+):([^/]+)/([^/]+?)(?:/|.git)?$'),
|
||||
},
|
||||
{
|
||||
protocol: 'ssh',
|
||||
regex: new RegExp('^git:(.+)/(.+)/(.+?)(?:/|.git)?$'),
|
||||
regex: new RegExp('^git:(.+)/([^/]+)/([^/]+?)(?:/|.git)?$'),
|
||||
},
|
||||
{
|
||||
protocol: 'ssh',
|
||||
|
|
|
@ -1,11 +1,33 @@
|
|||
import { debounce } from 'lodash'
|
||||
import React, { Component } from 'react'
|
||||
|
||||
interface IAriaLiveContainerProps {
|
||||
/**
|
||||
* Whether or not the component should make an invisible change to the content
|
||||
* in order to force the screen reader to read the content again.
|
||||
* There is a common pattern that we may need to announce a message in
|
||||
* response to user input. Unfortunately, aria-live announcements are
|
||||
* interrupted by continued user input. We can force a rereading of a message
|
||||
* by appending an invisible character when the user finishes their input.
|
||||
*
|
||||
* For example, we have a search filter for a list of branches and we need to
|
||||
* announce how may results are found. Say a list of branches and the user
|
||||
* types "ma", the message becomes "1 result", but if they continue to type
|
||||
* "main" the message will have been interrupted.
|
||||
*
|
||||
* This prop allows us to pass in when the user input changes. This can either
|
||||
* be directly passing in the user input on change or a boolean representing
|
||||
* when we want the message re-read. We can append the invisible character to
|
||||
* force the screen reader to read the message again after each input. To
|
||||
* prevent the message from being read too much, we debounce the message.
|
||||
*/
|
||||
readonly shouldForceChange?: boolean
|
||||
readonly trackedUserInput?: string | boolean
|
||||
|
||||
/** Optional id that can be used to associate the message to a control */
|
||||
readonly id?: string
|
||||
}
|
||||
|
||||
interface IAriaLiveContainerState {
|
||||
/** The generated message for the screen reader */
|
||||
readonly message: JSX.Element | null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,26 +40,55 @@ interface IAriaLiveContainerProps {
|
|||
* the screen reader to read the content again. This is useful when the content
|
||||
* is the same but the screen reader should read it again.
|
||||
*/
|
||||
export class AriaLiveContainer extends Component<IAriaLiveContainerProps> {
|
||||
private shouldForceChange: boolean = false
|
||||
export class AriaLiveContainer extends Component<
|
||||
IAriaLiveContainerProps,
|
||||
IAriaLiveContainerState
|
||||
> {
|
||||
private suffix: string = ''
|
||||
private onTrackedInputChanged = debounce((message: JSX.Element | null) => {
|
||||
this.setState({ message })
|
||||
}, 1000)
|
||||
|
||||
public constructor(props: IAriaLiveContainerProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
message: null,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IAriaLiveContainerProps) {
|
||||
this.shouldForceChange = prevProps.shouldForceChange ?? false
|
||||
if (prevProps.trackedUserInput === this.props.trackedUserInput) {
|
||||
return
|
||||
}
|
||||
|
||||
this.onTrackedInputChanged(this.buildMessage())
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.onTrackedInputChanged.cancel()
|
||||
}
|
||||
|
||||
private buildMessage() {
|
||||
this.suffix = this.suffix === '' ? '\u00A0' : ''
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.props.children}
|
||||
{this.suffix}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const shouldForceChange = this.shouldForceChange
|
||||
this.shouldForceChange = false
|
||||
|
||||
if (shouldForceChange) {
|
||||
this.suffix = this.suffix === '' ? '\u00A0' : ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sr-only" aria-live="polite" aria-atomic="true">
|
||||
{this.props.children}
|
||||
{this.suffix}
|
||||
<div
|
||||
id={this.props.id}
|
||||
className="sr-only"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{this.state.message}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import * as Path from 'path'
|
||||
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import {
|
||||
IAppState,
|
||||
|
@ -2424,6 +2426,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
showSideBySideDiff={showSideBySideDiff}
|
||||
currentBranchHasPullRequest={currentBranchHasPullRequest}
|
||||
onDismissed={onPopupDismissedFn}
|
||||
onOpenInExternalEditor={this.onOpenInExternalEditor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -2761,6 +2764,16 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
this.props.dispatcher.openInExternalEditor(repository.path)
|
||||
}
|
||||
|
||||
private onOpenInExternalEditor = (path: string) => {
|
||||
const repository = this.state.selectedState?.repository
|
||||
if (repository === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const fullPath = Path.join(repository.path, path)
|
||||
this.props.dispatcher.openInExternalEditor(fullPath)
|
||||
}
|
||||
|
||||
private showRepository = (repository: Repository | CloningRepository) => {
|
||||
if (!(repository instanceof Repository)) {
|
||||
return
|
||||
|
@ -2908,6 +2921,10 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
|
||||
if (tip.kind === TipState.Valid && tip.branch.upstreamRemoteName !== null) {
|
||||
remoteName = tip.branch.upstreamRemoteName
|
||||
|
||||
if (tip.branch.upstreamWithoutRemote !== tip.branch.name) {
|
||||
remoteName = tip.branch.upstream
|
||||
}
|
||||
}
|
||||
|
||||
const currentFoldout = this.state.currentFoldout
|
||||
|
@ -3188,7 +3205,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
accounts={state.accounts}
|
||||
externalEditorLabel={externalEditorLabel}
|
||||
resolvedExternalEditor={state.resolvedExternalEditor}
|
||||
onOpenInExternalEditor={this.openFileInExternalEditor}
|
||||
onOpenInExternalEditor={this.onOpenInExternalEditor}
|
||||
appMenu={state.appMenuState[0]}
|
||||
currentTutorialStep={state.currentOnboardingTutorialStep}
|
||||
onExitTutorial={this.onExitTutorial}
|
||||
|
|
|
@ -8,16 +8,21 @@ import {
|
|||
import { IAutocompletionProvider } from './index'
|
||||
import { fatalError } from '../../lib/fatal-error'
|
||||
import classNames from 'classnames'
|
||||
import getCaretCoordinates from 'textarea-caret'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { AriaLiveContainer } from '../accessibility/aria-live-container'
|
||||
import { createUniqueId, releaseUniqueId } from '../lib/id-pool'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchorPosition,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
|
||||
interface IRange {
|
||||
readonly start: number
|
||||
readonly length: number
|
||||
}
|
||||
|
||||
import getCaretCoordinates from 'textarea-caret'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { AriaLiveContainer } from '../accessibility/aria-live-container'
|
||||
|
||||
interface IAutocompletingTextInputProps<ElementType, AutocompleteItemType> {
|
||||
/**
|
||||
* An optional className to be applied to the rendered
|
||||
|
@ -28,6 +33,9 @@ interface IAutocompletingTextInputProps<ElementType, AutocompleteItemType> {
|
|||
/** Element ID for the input field. */
|
||||
readonly elementId?: string
|
||||
|
||||
/** Content of an optional invisible label element for screen readers. */
|
||||
readonly screenReaderLabel?: string
|
||||
|
||||
/** The placeholder for the input field. */
|
||||
readonly placeholder?: string
|
||||
|
||||
|
@ -40,12 +48,6 @@ interface IAutocompletingTextInputProps<ElementType, AutocompleteItemType> {
|
|||
/** Indicates if input field should be required */
|
||||
readonly required?: boolean
|
||||
|
||||
/**
|
||||
* Indicates if input field should be considered a combobox by assistive
|
||||
* technologies. Optional. Default: false
|
||||
*/
|
||||
readonly isCombobox?: boolean
|
||||
|
||||
/** Indicates if input field applies spellcheck */
|
||||
readonly spellcheck?: boolean
|
||||
|
||||
|
@ -121,6 +123,24 @@ interface IAutocompletingTextInputState<T> {
|
|||
* matching autocompletion providers.
|
||||
*/
|
||||
readonly autocompletionState: IAutocompletionState<T> | null
|
||||
|
||||
/** Coordinates of the caret in the input/textarea element */
|
||||
readonly caretCoordinates: ReturnType<typeof getCaretCoordinates> | null
|
||||
|
||||
/**
|
||||
* An automatically generated id for the text element used to reference
|
||||
* it from the label element. This is generated once via the id pool when the
|
||||
* component is mounted and then released once the component unmounts.
|
||||
*/
|
||||
readonly uniqueInternalElementId?: string
|
||||
|
||||
/**
|
||||
* An automatically generated id for the autocomplete container element used
|
||||
* to reference it from the ARIA autocomplete-related attributes. This is
|
||||
* generated once via the id pool when the component is mounted and then
|
||||
* released once the component unmounts.
|
||||
*/
|
||||
readonly autocompleteContainerId?: string
|
||||
}
|
||||
|
||||
/** A text area which provides autocompletions as the user types. */
|
||||
|
@ -132,7 +152,7 @@ export abstract class AutocompletingTextInput<
|
|||
IAutocompletingTextInputState<AutocompleteItemType>
|
||||
> {
|
||||
private element: ElementType | null = null
|
||||
private shouldForceAriaLiveMessage = false
|
||||
private invisibleCaretRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
/** The identifier for each autocompletion request. */
|
||||
private autocompletionRequestID = 0
|
||||
|
@ -150,6 +170,27 @@ export abstract class AutocompletingTextInput<
|
|||
|
||||
this.state = {
|
||||
autocompletionState: null,
|
||||
caretCoordinates: null,
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillMount() {
|
||||
const elementId = createUniqueId('autocompleting-text-input')
|
||||
const autocompleteContainerId = createUniqueId('autocomplete-container')
|
||||
|
||||
this.setState({
|
||||
uniqueInternalElementId: elementId,
|
||||
autocompleteContainerId,
|
||||
})
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.state.uniqueInternalElementId) {
|
||||
releaseUniqueId(this.state.uniqueInternalElementId)
|
||||
}
|
||||
|
||||
if (this.state.autocompleteContainerId) {
|
||||
releaseUniqueId(this.state.autocompleteContainerId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,6 +205,10 @@ export abstract class AutocompletingTextInput<
|
|||
}
|
||||
}
|
||||
|
||||
private get elementId() {
|
||||
return this.props.elementId ?? this.state.uniqueInternalElementId
|
||||
}
|
||||
|
||||
private renderItem = (row: number): JSX.Element | null => {
|
||||
const state = this.state.autocompletionState
|
||||
if (!state) {
|
||||
|
@ -190,45 +235,10 @@ export abstract class AutocompletingTextInput<
|
|||
return null
|
||||
}
|
||||
|
||||
const element = this.element!
|
||||
let coordinates = getCaretCoordinates(element, state.range.start)
|
||||
coordinates = {
|
||||
...coordinates,
|
||||
top: coordinates.top - element.scrollTop,
|
||||
left: coordinates.left - element.scrollLeft,
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
const popupAbsoluteTop = rect.top + coordinates.top
|
||||
const popupAbsoluteLeft = rect.left + coordinates.left
|
||||
const left = popupAbsoluteLeft
|
||||
const selectedRow = state.selectedItem
|
||||
? items.indexOf(state.selectedItem)
|
||||
: -1
|
||||
|
||||
// The maximum height we can use for the popup without it extending beyond
|
||||
// the Window bounds.
|
||||
let maxHeight: number
|
||||
let belowElement: boolean = true
|
||||
if (
|
||||
element.ownerDocument !== null &&
|
||||
element.ownerDocument.defaultView !== null
|
||||
) {
|
||||
const windowHeight = element.ownerDocument.defaultView.innerHeight
|
||||
const spaceToBottomOfWindow = windowHeight - popupAbsoluteTop - YOffset
|
||||
if (
|
||||
spaceToBottomOfWindow < DefaultPopupHeight &&
|
||||
popupAbsoluteTop >= DefaultPopupHeight
|
||||
) {
|
||||
maxHeight = DefaultPopupHeight
|
||||
belowElement = false
|
||||
} else {
|
||||
maxHeight = Math.min(DefaultPopupHeight, spaceToBottomOfWindow)
|
||||
}
|
||||
} else {
|
||||
maxHeight = DefaultPopupHeight
|
||||
}
|
||||
|
||||
// The height needed to accommodate all the matched items without overflowing
|
||||
//
|
||||
// Magic number warning! The autocompletion-popup container adds a border
|
||||
|
@ -236,8 +246,7 @@ export abstract class AutocompletingTextInput<
|
|||
// without overflowing and triggering the scrollbar.
|
||||
const noOverflowItemHeight = RowHeight * items.length
|
||||
|
||||
const height = Math.min(noOverflowItemHeight, maxHeight)
|
||||
const top = popupAbsoluteTop + (belowElement ? YOffset + 1 : -height)
|
||||
const minHeight = RowHeight * Math.min(items.length, 3)
|
||||
|
||||
// Use the completion text as invalidation props so that highlighting
|
||||
// will update as you type even though the number of items matched
|
||||
|
@ -249,9 +258,17 @@ export abstract class AutocompletingTextInput<
|
|||
const className = classNames('autocompletion-popup', state.provider.kind)
|
||||
|
||||
return (
|
||||
<div className={className} style={{ top, left, height }}>
|
||||
<Popover
|
||||
anchor={this.invisibleCaretRef.current}
|
||||
anchorPosition={PopoverAnchorPosition.BottomLeft}
|
||||
decoration={PopoverDecoration.None}
|
||||
maxHeight={Math.min(DefaultPopupHeight, noOverflowItemHeight)}
|
||||
minHeight={minHeight}
|
||||
trapFocus={false}
|
||||
className={className}
|
||||
>
|
||||
<List
|
||||
accessibleListId="autocomplete-container"
|
||||
accessibleListId={this.state.autocompleteContainerId}
|
||||
ref={this.onAutocompletionListRef}
|
||||
rowCount={items.length}
|
||||
rowHeight={RowHeight}
|
||||
|
@ -264,7 +281,7 @@ export abstract class AutocompletingTextInput<
|
|||
onSelectedRowChanged={this.onSelectedRowChanged}
|
||||
invalidationProps={searchText}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -377,8 +394,8 @@ export abstract class AutocompletingTextInput<
|
|||
|
||||
const props = {
|
||||
type: 'text',
|
||||
id: this.props.elementId,
|
||||
role: this.props.isCombobox ? ('combobox' as const) : undefined,
|
||||
id: this.elementId,
|
||||
role: 'combobox',
|
||||
placeholder: this.props.placeholder,
|
||||
value: this.props.value,
|
||||
ref: this.onRef,
|
||||
|
@ -394,8 +411,8 @@ export abstract class AutocompletingTextInput<
|
|||
'aria-expanded': autocompleteVisible,
|
||||
'aria-autocomplete': 'list' as const,
|
||||
'aria-haspopup': 'listbox' as const,
|
||||
'aria-controls': 'autocomplete-container',
|
||||
'aria-owns': 'autocomplete-container',
|
||||
'aria-controls': this.state.autocompleteContainerId,
|
||||
'aria-owns': this.state.autocompleteContainerId,
|
||||
'aria-activedescendant': this.getActiveAutocompleteItemId(),
|
||||
}
|
||||
|
||||
|
@ -405,6 +422,53 @@ export abstract class AutocompletingTextInput<
|
|||
)
|
||||
}
|
||||
|
||||
private updateCaretCoordinates = () => {
|
||||
const element = this.element
|
||||
if (!element) {
|
||||
this.setState({ caretCoordinates: null })
|
||||
return
|
||||
}
|
||||
|
||||
const selectionEnd = element.selectionEnd
|
||||
if (selectionEnd === null) {
|
||||
this.setState({ caretCoordinates: null })
|
||||
return
|
||||
}
|
||||
|
||||
const caretCoordinates = getCaretCoordinates(element, selectionEnd)
|
||||
|
||||
this.setState({
|
||||
caretCoordinates: {
|
||||
top: caretCoordinates.top - element.scrollTop,
|
||||
left: caretCoordinates.left - element.scrollLeft,
|
||||
height: caretCoordinates.height,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private renderInvisibleCaret = () => {
|
||||
const { caretCoordinates } = this.state
|
||||
if (!caretCoordinates) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
width: 2,
|
||||
height: YOffset,
|
||||
position: 'absolute',
|
||||
left: caretCoordinates.left,
|
||||
top: caretCoordinates.top,
|
||||
}}
|
||||
ref={this.invisibleCaretRef}
|
||||
>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onBlur = (e: React.FocusEvent<ElementType>) => {
|
||||
this.close()
|
||||
}
|
||||
|
@ -421,6 +485,7 @@ export abstract class AutocompletingTextInput<
|
|||
|
||||
private onRef = (ref: ElementType | null) => {
|
||||
this.element = ref
|
||||
this.updateCaretCoordinates()
|
||||
if (this.props.onElementRef) {
|
||||
this.props.onElementRef(ref)
|
||||
}
|
||||
|
@ -444,9 +509,6 @@ export abstract class AutocompletingTextInput<
|
|||
}
|
||||
)
|
||||
|
||||
const shouldForceAriaLiveMessage = this.shouldForceAriaLiveMessage
|
||||
this.shouldForceAriaLiveMessage = false
|
||||
|
||||
const autoCompleteItems = this.state.autocompletionState?.items ?? []
|
||||
|
||||
const suggestionsMessage =
|
||||
|
@ -457,8 +519,16 @@ export abstract class AutocompletingTextInput<
|
|||
return (
|
||||
<div className={className}>
|
||||
{this.renderAutocompletions()}
|
||||
{this.props.screenReaderLabel && (
|
||||
<label className="sr-only" htmlFor={this.elementId}>
|
||||
{this.props.screenReaderLabel}
|
||||
</label>
|
||||
)}
|
||||
{this.renderTextInput()}
|
||||
<AriaLiveContainer shouldForceChange={shouldForceAriaLiveMessage}>
|
||||
{this.renderInvisibleCaret()}
|
||||
<AriaLiveContainer
|
||||
trackedUserInput={this.state.autocompletionState?.rangeText}
|
||||
>
|
||||
{autoCompleteItems.length > 0 ? suggestionsMessage : ''}
|
||||
</AriaLiveContainer>
|
||||
</div>
|
||||
|
@ -651,6 +721,8 @@ export abstract class AutocompletingTextInput<
|
|||
this.props.onValueChanged(str)
|
||||
}
|
||||
|
||||
this.updateCaretCoordinates()
|
||||
|
||||
return this.open(str)
|
||||
}
|
||||
|
||||
|
@ -679,7 +751,6 @@ export abstract class AutocompletingTextInput<
|
|||
return
|
||||
}
|
||||
|
||||
this.shouldForceAriaLiveMessage = true
|
||||
this.setState({ autocompletionState })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,11 @@ import { IMatches } from '../../lib/fuzzy-find'
|
|||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { HighlightText } from '../lib/highlight-text'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { dragAndDropManager } from '../../lib/drag-and-drop-manager'
|
||||
import { DragType, DropTargetType } from '../../models/drag-drop'
|
||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||
import { RelativeTime } from '../relative-time'
|
||||
import classNames from 'classnames'
|
||||
import { generateBranchContextMenuItems } from './branch-list-item-context-menu'
|
||||
|
||||
interface IBranchListItemProps {
|
||||
/** The name of the branch */
|
||||
|
@ -26,13 +24,6 @@ interface IBranchListItemProps {
|
|||
/** The characters in the branch name to highlight */
|
||||
readonly matches: IMatches
|
||||
|
||||
/** Specifies whether the branch is local */
|
||||
readonly isLocal: boolean
|
||||
|
||||
readonly onRenameBranch?: (branchName: string) => void
|
||||
|
||||
readonly onDeleteBranch?: (branchName: string) => void
|
||||
|
||||
/** When a drag element has landed on a branch that is not current */
|
||||
readonly onDropOntoBranch?: (branchName: string) => void
|
||||
|
||||
|
@ -59,30 +50,6 @@ export class BranchListItem extends React.Component<
|
|||
this.state = { isDragInProgress: false }
|
||||
}
|
||||
|
||||
private onContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault()
|
||||
|
||||
/*
|
||||
There are multiple instances in the application where a branch list item
|
||||
is rendered. We only want to be able to rename or delete them on the
|
||||
branch dropdown menu. Thus, other places simply will not provide these
|
||||
methods, such as the merge and rebase logic.
|
||||
*/
|
||||
const { onRenameBranch, onDeleteBranch, name, isLocal } = this.props
|
||||
if (onRenameBranch === undefined && onDeleteBranch === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const items = generateBranchContextMenuItems({
|
||||
name,
|
||||
isLocal,
|
||||
onRenameBranch,
|
||||
onDeleteBranch,
|
||||
})
|
||||
|
||||
showContextualMenu(items)
|
||||
}
|
||||
|
||||
private onMouseEnter = () => {
|
||||
if (dragAndDropManager.isDragInProgress) {
|
||||
this.setState({ isDragInProgress: true })
|
||||
|
@ -133,7 +100,6 @@ export class BranchListItem extends React.Component<
|
|||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
onContextMenu={this.onContextMenu}
|
||||
className={className}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { Branch } from '../../models/branch'
|
||||
import { Branch, BranchType } from '../../models/branch'
|
||||
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
|
||||
|
@ -20,6 +20,8 @@ import {
|
|||
} from './group-branches'
|
||||
import { NoBranches } from './no-branches'
|
||||
import { SelectionDirection, ClickSource } from '../lib/list'
|
||||
import { generateBranchContextMenuItems } from './branch-list-item-context-menu'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
|
||||
const RowHeight = 30
|
||||
|
||||
|
@ -113,6 +115,12 @@ interface IBranchListProps {
|
|||
|
||||
/** Optional: No branches message */
|
||||
readonly noBranchesMessage?: string | JSX.Element
|
||||
|
||||
/** Optional: Callback for if rename context menu should exist */
|
||||
readonly onRenameBranch?: (branchName: string) => void
|
||||
|
||||
/** Optional: Callback for if delete context menu should exist */
|
||||
readonly onDeleteBranch?: (branchName: string) => void
|
||||
}
|
||||
|
||||
interface IBranchListState {
|
||||
|
@ -200,10 +208,34 @@ export class BranchList extends React.Component<
|
|||
hideFilterRow={this.props.hideFilterRow}
|
||||
onFilterListResultsChanged={this.props.onFilterListResultsChanged}
|
||||
renderPreList={this.props.renderPreList}
|
||||
onItemContextMenu={this.onBranchContextMenu}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private onBranchContextMenu = (
|
||||
item: IBranchListItem,
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
event.preventDefault()
|
||||
|
||||
const { onRenameBranch, onDeleteBranch } = this.props
|
||||
if (onRenameBranch === undefined && onDeleteBranch === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const { type, name } = item.branch
|
||||
const isLocal = type === BranchType.Local
|
||||
const items = generateBranchContextMenuItems({
|
||||
name,
|
||||
isLocal,
|
||||
onRenameBranch,
|
||||
onDeleteBranch,
|
||||
})
|
||||
|
||||
showContextualMenu(items)
|
||||
}
|
||||
|
||||
private onBranchesFilterListRef = (
|
||||
filterList: FilterList<IBranchListItem> | null
|
||||
) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { Branch, BranchType } from '../../models/branch'
|
||||
import { Branch } from '../../models/branch'
|
||||
|
||||
import { IBranchListItem } from './group-branches'
|
||||
import { BranchListItem } from './branch-list-item'
|
||||
|
@ -10,8 +10,6 @@ export function renderDefaultBranch(
|
|||
item: IBranchListItem,
|
||||
matches: IMatches,
|
||||
currentBranch: Branch | null,
|
||||
onRenameBranch?: (branchName: string) => void,
|
||||
onDeleteBranch?: (branchName: string) => void,
|
||||
onDropOntoBranch?: (branchName: string) => void,
|
||||
onDropOntoCurrentBranch?: () => void
|
||||
): JSX.Element {
|
||||
|
@ -22,11 +20,8 @@ export function renderDefaultBranch(
|
|||
<BranchListItem
|
||||
name={branch.name}
|
||||
isCurrentBranch={branch.name === currentBranchName}
|
||||
isLocal={branch.type === BranchType.Local}
|
||||
lastCommitDate={commit ? commit.author.date : null}
|
||||
matches={matches}
|
||||
onRenameBranch={onRenameBranch}
|
||||
onDeleteBranch={onDeleteBranch}
|
||||
onDropOntoBranch={onDropOntoBranch}
|
||||
onDropOntoCurrentBranch={onDropOntoCurrentBranch}
|
||||
/>
|
||||
|
|
|
@ -206,8 +206,6 @@ export class BranchesContainer extends React.Component<
|
|||
item,
|
||||
matches,
|
||||
this.props.currentBranch,
|
||||
this.props.onRenameBranch,
|
||||
this.props.onDeleteBranch,
|
||||
this.onDropOntoBranch,
|
||||
this.onDropOntoCurrentBranch
|
||||
)
|
||||
|
@ -239,6 +237,8 @@ export class BranchesContainer extends React.Component<
|
|||
DragType.Commit
|
||||
)}
|
||||
renderPreList={this.renderPreList}
|
||||
onRenameBranch={this.props.onRenameBranch}
|
||||
onDeleteBranch={this.props.onDeleteBranch}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ interface IPullRequestBadgeProps {
|
|||
/** The GitHub repository to use when looking up commit status. */
|
||||
readonly repository: GitHubRepository
|
||||
|
||||
readonly onBadgeRef?: (ref: HTMLDivElement | null) => void
|
||||
|
||||
/** The GitHub repository to use when looking up commit status. */
|
||||
readonly onBadgeClick?: () => void
|
||||
|
||||
|
@ -57,6 +59,7 @@ export class PullRequestBadge extends React.Component<
|
|||
|
||||
private onRef = (badgeRef: HTMLDivElement) => {
|
||||
this.badgeRef = badgeRef
|
||||
this.props.onBadgeRef?.(badgeRef)
|
||||
}
|
||||
|
||||
private onBadgeClick = (
|
||||
|
|
|
@ -36,13 +36,17 @@ export class ChangeRepositoryAlias extends React.Component<
|
|||
title={
|
||||
__DARWIN__ ? `${verb} Repository Alias` : `${verb} repository alias`
|
||||
}
|
||||
ariaDescribedBy="change-repository-alias-description"
|
||||
onDismissed={this.props.onDismissed}
|
||||
onSubmit={this.changeAlias}
|
||||
>
|
||||
<DialogContent>
|
||||
<p>Choose a new alias for the repository "{nameOf(repository)}". </p>
|
||||
<p id="change-repository-alias-description">
|
||||
Choose a new alias for the repository "{nameOf(repository)}".{' '}
|
||||
</p>
|
||||
<p>
|
||||
<TextBox
|
||||
ariaLabel="Alias"
|
||||
value={this.state.newAlias}
|
||||
onValueChanged={this.onNameChanged}
|
||||
/>
|
||||
|
|
|
@ -146,11 +146,19 @@ interface IChangesListProps {
|
|||
readonly changesListScrollTop?: number
|
||||
|
||||
/**
|
||||
* Called to open a file it its default application
|
||||
* Called to open a file in its default application
|
||||
*
|
||||
* @param path The path of the file relative to the root of the repository
|
||||
*/
|
||||
readonly onOpenItem: (path: string) => void
|
||||
|
||||
/**
|
||||
* Called to open a file in the default external editor
|
||||
*
|
||||
* @param path The path of the file relative to the root of the repository
|
||||
*/
|
||||
readonly onOpenItemInExternalEditor: (path: string) => void
|
||||
|
||||
/**
|
||||
* The currently checked out branch (null if no branch is checked out).
|
||||
*/
|
||||
|
@ -196,13 +204,6 @@ interface IChangesListProps {
|
|||
/** The name of the currently selected external editor */
|
||||
readonly externalEditorLabel?: string
|
||||
|
||||
/**
|
||||
* Callback to open a selected file using the configured external editor
|
||||
*
|
||||
* @param fullPath The full path to the file on disk
|
||||
*/
|
||||
readonly onOpenInExternalEditor: (fullPath: string) => void
|
||||
|
||||
readonly stashEntry: IStashEntry | null
|
||||
|
||||
readonly isShowingStashEntry: boolean
|
||||
|
@ -493,7 +494,7 @@ export class ChangesList extends React.Component<
|
|||
file: WorkingDirectoryFileChange,
|
||||
enabled: boolean
|
||||
): IMenuItem => {
|
||||
const { externalEditorLabel, repository } = this.props
|
||||
const { externalEditorLabel } = this.props
|
||||
|
||||
const openInExternalEditor = externalEditorLabel
|
||||
? `Open in ${externalEditorLabel}`
|
||||
|
@ -502,8 +503,7 @@ export class ChangesList extends React.Component<
|
|||
return {
|
||||
label: openInExternalEditor,
|
||||
action: () => {
|
||||
const fullPath = Path.join(repository.path, file.path)
|
||||
this.props.onOpenInExternalEditor(fullPath)
|
||||
this.props.onOpenItemInExternalEditor(file.path)
|
||||
},
|
||||
enabled,
|
||||
}
|
||||
|
@ -901,6 +901,12 @@ export class ChangesList extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private onRowDoubleClick = (row: number) => {
|
||||
const file = this.props.workingDirectory.files[row]
|
||||
|
||||
this.props.onOpenItemInExternalEditor(file.path)
|
||||
}
|
||||
|
||||
private onRowKeyDown = (
|
||||
_row: number,
|
||||
event: React.KeyboardEvent<HTMLDivElement>
|
||||
|
@ -981,6 +987,7 @@ export class ChangesList extends React.Component<
|
|||
isCommitting: isCommitting,
|
||||
}}
|
||||
onRowClick={this.props.onRowClick}
|
||||
onRowDoubleClick={this.onRowDoubleClick}
|
||||
onScroll={this.onScroll}
|
||||
setScrollTop={this.props.changesListScrollTop}
|
||||
onRowKeyDown={this.onRowKeyDown}
|
||||
|
|
|
@ -23,6 +23,13 @@ interface IChangesProps {
|
|||
readonly isCommitting: boolean
|
||||
readonly hideWhitespaceInDiff: boolean
|
||||
|
||||
/**
|
||||
* Callback to open a selected file using the configured external editor
|
||||
*
|
||||
* @param fullPath The full path to the file on disk
|
||||
*/
|
||||
readonly onOpenInExternalEditor: (fullPath: string) => void
|
||||
|
||||
/**
|
||||
* Called when the user requests to open a binary file in an the
|
||||
* system-assigned application for said file type.
|
||||
|
|
|
@ -2,20 +2,29 @@ import React from 'react'
|
|||
import { Select } from '../lib/select'
|
||||
import { Button } from '../lib/button'
|
||||
import { Row } from '../lib/row'
|
||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchorPosition,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
import { IAvatarUser } from '../../models/avatar'
|
||||
import { Avatar } from '../lib/avatar'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { LinkButton } from '../lib/link-button'
|
||||
import { ToggledtippedContent } from '../lib/toggletipped-content'
|
||||
import { TooltipDirection } from '../lib/tooltip'
|
||||
import { OkCancelButtonGroup } from '../dialog'
|
||||
import { getConfigValue } from '../../lib/git/config'
|
||||
import { Repository } from '../../models/repository'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface ICommitMessageAvatarState {
|
||||
readonly isPopoverOpen: boolean
|
||||
|
||||
/** Currently selected account email address. */
|
||||
readonly accountEmail: string
|
||||
|
||||
/** Whether the git configuration is local to the repository or global */
|
||||
readonly isGitConfigLocal: boolean
|
||||
}
|
||||
|
||||
interface ICommitMessageAvatarProps {
|
||||
|
@ -37,6 +46,11 @@ interface ICommitMessageAvatarProps {
|
|||
/** Preferred email address from the user's account. */
|
||||
readonly preferredAccountEmail: string
|
||||
|
||||
/**
|
||||
* The currently selected repository
|
||||
*/
|
||||
readonly repository: Repository
|
||||
|
||||
readonly onUpdateEmail: (email: string) => void
|
||||
|
||||
/**
|
||||
|
@ -44,6 +58,12 @@ interface ICommitMessageAvatarProps {
|
|||
* repository settings dialog
|
||||
*/
|
||||
readonly onOpenRepositorySettings: () => void
|
||||
|
||||
/**
|
||||
* Called when the user has requested to see the Git tab in the user settings
|
||||
* dialog
|
||||
*/
|
||||
readonly onOpenGitSettings: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,7 +75,7 @@ export class CommitMessageAvatar extends React.Component<
|
|||
ICommitMessageAvatarState
|
||||
> {
|
||||
private avatarButtonRef: HTMLButtonElement | null = null
|
||||
private popoverRef = React.createRef<Popover>()
|
||||
private warningBadgeRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
public constructor(props: ICommitMessageAvatarProps) {
|
||||
super(props)
|
||||
|
@ -63,27 +83,30 @@ export class CommitMessageAvatar extends React.Component<
|
|||
this.state = {
|
||||
isPopoverOpen: false,
|
||||
accountEmail: this.props.preferredAccountEmail,
|
||||
isGitConfigLocal: false,
|
||||
}
|
||||
this.determineGitConfigLocation()
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: ICommitMessageAvatarProps) {
|
||||
if (
|
||||
this.props.user?.name !== prevProps.user?.name ||
|
||||
this.props.user?.email !== prevProps.user?.email
|
||||
) {
|
||||
this.determineGitConfigLocation()
|
||||
}
|
||||
}
|
||||
|
||||
private getTitle(): string | JSX.Element | undefined {
|
||||
const { user } = this.props
|
||||
private async determineGitConfigLocation() {
|
||||
const isGitConfigLocal = await this.isGitConfigLocal()
|
||||
this.setState({ isGitConfigLocal })
|
||||
}
|
||||
|
||||
if (user === undefined) {
|
||||
return 'Unknown user'
|
||||
}
|
||||
|
||||
const { name, email } = user
|
||||
|
||||
if (user.name) {
|
||||
return (
|
||||
<>
|
||||
Committing as <strong>{name}</strong> {email}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return email
|
||||
private isGitConfigLocal = async () => {
|
||||
const { repository } = this.props
|
||||
const localName = await getConfigValue(repository, 'user.name', true)
|
||||
const localEmail = await getConfigValue(repository, 'user.email', true)
|
||||
return localName !== null || localEmail !== null
|
||||
}
|
||||
|
||||
private onButtonRef = (buttonRef: HTMLButtonElement | null) => {
|
||||
|
@ -91,30 +114,27 @@ export class CommitMessageAvatar extends React.Component<
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { warningBadgeVisible, user } = this.props
|
||||
|
||||
const ariaLabel = warningBadgeVisible
|
||||
? 'Commit may be misattributed. View warning.'
|
||||
: 'View commit author information'
|
||||
|
||||
const classes = classNames('commit-message-avatar-component', {
|
||||
misattributed: warningBadgeVisible,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="commit-message-avatar-component">
|
||||
{this.props.warningBadgeVisible && (
|
||||
<Button
|
||||
className="avatar-button"
|
||||
ariaLabel="Commit may be misattributed. View warning."
|
||||
onButtonRef={this.onButtonRef}
|
||||
onClick={this.onAvatarClick}
|
||||
>
|
||||
{this.renderWarningBadge()}
|
||||
<Avatar user={this.props.user} title={null} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!this.props.warningBadgeVisible && (
|
||||
<ToggledtippedContent
|
||||
tooltip={this.getTitle()}
|
||||
direction={TooltipDirection.NORTH}
|
||||
ariaLabel="Show Commit Author Details"
|
||||
>
|
||||
<Avatar user={this.props.user} title={null} />
|
||||
</ToggledtippedContent>
|
||||
)}
|
||||
|
||||
<div className={classes}>
|
||||
<Button
|
||||
className="avatar-button"
|
||||
ariaLabel={ariaLabel}
|
||||
onButtonRef={this.onButtonRef}
|
||||
onClick={this.onAvatarClick}
|
||||
>
|
||||
{warningBadgeVisible && this.renderWarningBadge()}
|
||||
<Avatar user={user} title={null} />
|
||||
</Button>
|
||||
{this.state.isPopoverOpen && this.renderPopover()}
|
||||
</div>
|
||||
)
|
||||
|
@ -122,7 +142,7 @@ export class CommitMessageAvatar extends React.Component<
|
|||
|
||||
private renderWarningBadge() {
|
||||
return (
|
||||
<div className="warning-badge">
|
||||
<div className="warning-badge" ref={this.warningBadgeRef}>
|
||||
<Octicon symbol={OcticonSymbol.alert} />
|
||||
</div>
|
||||
)
|
||||
|
@ -147,10 +167,6 @@ export class CommitMessageAvatar extends React.Component<
|
|||
}
|
||||
|
||||
private onAvatarClick = (event: React.FormEvent<HTMLButtonElement>) => {
|
||||
if (this.props.warningBadgeVisible === false) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
if (this.state.isPopoverOpen) {
|
||||
this.closePopover()
|
||||
|
@ -159,25 +175,48 @@ export class CommitMessageAvatar extends React.Component<
|
|||
}
|
||||
}
|
||||
|
||||
private getPopoverPosition(): React.CSSProperties | undefined {
|
||||
if (!this.avatarButtonRef) {
|
||||
return
|
||||
}
|
||||
private renderGitConfigPopover() {
|
||||
const { user } = this.props
|
||||
const { isGitConfigLocal } = this.state
|
||||
|
||||
const defaultPopoverHeight = 278
|
||||
const popoverHeight =
|
||||
this.popoverRef.current?.containerDivRef.current?.clientHeight ??
|
||||
defaultPopoverHeight
|
||||
const buttonHeight = this.avatarButtonRef.clientHeight
|
||||
const buttonWidth = this.avatarButtonRef.clientWidth
|
||||
const rect = this.avatarButtonRef.getBoundingClientRect()
|
||||
const top = rect.top - popoverHeight + buttonHeight / 2
|
||||
const left = rect.left + buttonWidth / 2
|
||||
const location = isGitConfigLocal ? 'local' : 'global'
|
||||
const locationDesc = isGitConfigLocal ? 'for your repository' : ''
|
||||
const settingsName = __DARWIN__ ? 'preferences' : 'options'
|
||||
const settings = isGitConfigLocal
|
||||
? 'repository settings'
|
||||
: `git ${settingsName}`
|
||||
const buttonText = __DARWIN__ ? 'Open Git Settings' : 'Open git settings'
|
||||
|
||||
return { top, left }
|
||||
return (
|
||||
<>
|
||||
<p>{user && user.name && `Email: ${user.email}`}</p>
|
||||
|
||||
<p>
|
||||
You can update your {location} git configuration {locationDesc} in
|
||||
your {settings}.
|
||||
</p>
|
||||
|
||||
{!isGitConfigLocal && (
|
||||
<p className="secondary-text">
|
||||
You can also set an email local to this repository from the{' '}
|
||||
<LinkButton onClick={this.onRepositorySettingsClick}>
|
||||
repository settings
|
||||
</LinkButton>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
<Row className="button-row">
|
||||
<OkCancelButtonGroup
|
||||
okButtonText={buttonText}
|
||||
onOkButtonClick={this.onOpenGitSettings}
|
||||
onCancelButtonClick={this.onIgnoreClick}
|
||||
/>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private renderPopover() {
|
||||
private renderMisattributedCommitPopover() {
|
||||
const accountTypeSuffix = this.props.isEnterpriseAccount
|
||||
? ' Enterprise'
|
||||
: ''
|
||||
|
@ -190,16 +229,7 @@ export class CommitMessageAvatar extends React.Component<
|
|||
: ''
|
||||
|
||||
return (
|
||||
<Popover
|
||||
caretPosition={PopoverCaretPosition.LeftBottom}
|
||||
onClickOutside={this.closePopover}
|
||||
ariaLabelledby="misattributed-commit-popover-header"
|
||||
style={this.getPopoverPosition()}
|
||||
ref={this.popoverRef}
|
||||
>
|
||||
<h3 id="misattributed-commit-popover-header">
|
||||
This commit will be misattributed
|
||||
</h3>
|
||||
<>
|
||||
<Row>
|
||||
<div>
|
||||
The email in your global Git config (
|
||||
|
@ -243,6 +273,54 @@ export class CommitMessageAvatar extends React.Component<
|
|||
{updateEmailTitle}
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private getCommittingAsTitle(): string | JSX.Element | undefined {
|
||||
const { user } = this.props
|
||||
|
||||
if (user === undefined) {
|
||||
return 'Unknown user'
|
||||
}
|
||||
|
||||
const { name, email } = user
|
||||
|
||||
if (name) {
|
||||
return (
|
||||
<>
|
||||
Committing as <strong>{name}</strong>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return <>Committing with {email}</>
|
||||
}
|
||||
|
||||
private renderPopover() {
|
||||
const { warningBadgeVisible } = this.props
|
||||
|
||||
return (
|
||||
<Popover
|
||||
anchor={
|
||||
warningBadgeVisible
|
||||
? this.warningBadgeRef.current
|
||||
: this.avatarButtonRef
|
||||
}
|
||||
anchorPosition={PopoverAnchorPosition.RightBottom}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
onClickOutside={this.closePopover}
|
||||
ariaLabelledby="commit-avatar-popover-header"
|
||||
>
|
||||
<h3 id="commit-avatar-popover-header">
|
||||
{warningBadgeVisible
|
||||
? 'This commit will be misattributed'
|
||||
: this.getCommittingAsTitle()}
|
||||
</h3>
|
||||
|
||||
{warningBadgeVisible
|
||||
? this.renderMisattributedCommitPopover()
|
||||
: this.renderGitConfigPopover()}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
@ -252,6 +330,15 @@ export class CommitMessageAvatar extends React.Component<
|
|||
this.props.onOpenRepositorySettings()
|
||||
}
|
||||
|
||||
private onOpenGitSettings = () => {
|
||||
this.closePopover()
|
||||
if (this.state.isGitConfigLocal) {
|
||||
this.props.onOpenRepositorySettings()
|
||||
} else {
|
||||
this.props.onOpenGitSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private onIgnoreClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
this.closePopover()
|
||||
|
|
|
@ -36,6 +36,7 @@ import { isEmptyOrWhitespace } from '../../lib/is-empty-or-whitespace'
|
|||
import { TooltipDirection } from '../lib/tooltip'
|
||||
import { pick } from '../../lib/pick'
|
||||
import { ToggledtippedContent } from '../lib/toggletipped-content'
|
||||
import { PreferencesTab } from '../../models/preferences'
|
||||
|
||||
const addAuthorIcon = {
|
||||
w: 18,
|
||||
|
@ -457,6 +458,8 @@ export class CommitMessage extends React.Component<
|
|||
}
|
||||
onUpdateEmail={this.onUpdateUserEmail}
|
||||
onOpenRepositorySettings={this.onOpenRepositorySettings}
|
||||
onOpenGitSettings={this.onOpenGitSettings}
|
||||
repository={repository}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -474,6 +477,13 @@ export class CommitMessage extends React.Component<
|
|||
})
|
||||
}
|
||||
|
||||
private onOpenGitSettings = () => {
|
||||
this.props.onShowPopup({
|
||||
type: PopupType.Preferences,
|
||||
initialSelectedTab: PreferencesTab.Git,
|
||||
})
|
||||
}
|
||||
|
||||
private get isCoAuthorInputEnabled() {
|
||||
return this.props.repository.gitHubRepository !== null
|
||||
}
|
||||
|
@ -861,6 +871,8 @@ export class CommitMessage extends React.Component<
|
|||
'nudge-arrow-left': this.props.shouldNudge === true,
|
||||
})
|
||||
|
||||
const { placeholder, isCommitting, commitSpellcheckEnabled } = this.props
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<div
|
||||
|
@ -875,8 +887,9 @@ export class CommitMessage extends React.Component<
|
|||
|
||||
<AutocompletingInput
|
||||
required={true}
|
||||
screenReaderLabel="Commit summary"
|
||||
className={summaryInputClassName}
|
||||
placeholder={this.props.placeholder}
|
||||
placeholder={placeholder}
|
||||
value={this.state.summary}
|
||||
onValueChanged={this.onSummaryChanged}
|
||||
onElementRef={this.onSummaryInputRef}
|
||||
|
@ -884,8 +897,8 @@ export class CommitMessage extends React.Component<
|
|||
this.state.commitMessageAutocompletionProviders
|
||||
}
|
||||
onContextMenu={this.onAutocompletingInputContextMenu}
|
||||
disabled={this.props.isCommitting === true}
|
||||
spellcheck={this.props.commitSpellcheckEnabled}
|
||||
disabled={isCommitting === true}
|
||||
spellcheck={commitSpellcheckEnabled}
|
||||
/>
|
||||
{showSummaryLengthHint && this.renderSummaryLengthHint()}
|
||||
</div>
|
||||
|
@ -896,6 +909,7 @@ export class CommitMessage extends React.Component<
|
|||
>
|
||||
<AutocompletingTextArea
|
||||
className={descriptionClassName}
|
||||
screenReaderLabel="Commit description"
|
||||
placeholder="Description"
|
||||
value={this.state.description || ''}
|
||||
onValueChanged={this.onDescriptionChanged}
|
||||
|
@ -905,8 +919,8 @@ export class CommitMessage extends React.Component<
|
|||
ref={this.onDescriptionFieldRef}
|
||||
onElementRef={this.onDescriptionTextAreaRef}
|
||||
onContextMenu={this.onAutocompletingInputContextMenu}
|
||||
disabled={this.props.isCommitting === true}
|
||||
spellcheck={this.props.commitSpellcheckEnabled}
|
||||
disabled={isCommitting === true}
|
||||
spellcheck={commitSpellcheckEnabled}
|
||||
/>
|
||||
{this.renderActionBar()}
|
||||
</FocusContainer>
|
||||
|
|
|
@ -249,6 +249,14 @@ export class ChangesSidebar extends React.Component<IChangesSidebarProps, {}> {
|
|||
const fullPath = Path.join(this.props.repository.path, path)
|
||||
openFile(fullPath, this.props.dispatcher)
|
||||
}
|
||||
/**
|
||||
* Called to open a file in the default external editor
|
||||
*
|
||||
* @param path The path of the file relative to the root of the repository
|
||||
*/
|
||||
private onOpenItemInExternalEditor = (path: string) => {
|
||||
this.props.onOpenInExternalEditor(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection of a given working directory file.
|
||||
|
@ -413,7 +421,7 @@ export class ChangesSidebar extends React.Component<IChangesSidebarProps, {}> {
|
|||
showCoAuthoredBy={showCoAuthoredBy}
|
||||
coAuthors={coAuthors}
|
||||
externalEditorLabel={this.props.externalEditorLabel}
|
||||
onOpenInExternalEditor={this.props.onOpenInExternalEditor}
|
||||
onOpenItemInExternalEditor={this.onOpenItemInExternalEditor}
|
||||
onChangesListScrolled={this.props.onChangesListScrolled}
|
||||
changesListScrollTop={this.props.changesListScrollTop}
|
||||
stashEntry={this.props.changes.stashEntry}
|
||||
|
|
|
@ -12,7 +12,11 @@ import {
|
|||
} from '../../lib/ci-checks/ci-checks'
|
||||
import { Octicon, syncClockwise } from '../octicons'
|
||||
import { APICheckConclusion, IAPIWorkflowJobStep } from '../../lib/api'
|
||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchorPosition,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
import { CICheckRunList } from './ci-check-run-list'
|
||||
import { encodePathAsUrl } from '../../lib/path'
|
||||
import { PopupType } from '../../models/popup'
|
||||
|
@ -42,9 +46,7 @@ interface ICICheckRunPopoverProps {
|
|||
/** The pull request's number. */
|
||||
readonly prNumber: number
|
||||
|
||||
/** The bottom of the pull request badge so we can position popover relative
|
||||
* to it. */
|
||||
readonly badgeBottom: number
|
||||
readonly anchor: HTMLElement | null
|
||||
|
||||
/** Callback for when popover closes */
|
||||
readonly closePopover: (event?: MouseEvent) => void
|
||||
|
@ -207,20 +209,6 @@ export class CICheckRunPopover extends React.PureComponent<
|
|||
})
|
||||
}
|
||||
|
||||
private getPopoverPositioningStyles = (): React.CSSProperties => {
|
||||
const top = this.props.badgeBottom + 10
|
||||
return { top }
|
||||
}
|
||||
|
||||
private getListHeightStyles = (): React.CSSProperties => {
|
||||
const headerHeight = 55
|
||||
return {
|
||||
maxHeight: `${
|
||||
window.innerHeight - (this.props.badgeBottom + headerHeight + 20)
|
||||
}px`,
|
||||
}
|
||||
}
|
||||
|
||||
private renderRerunButton = () => {
|
||||
const { checkRuns } = this.state
|
||||
if (!supportsRerunningChecks(this.props.repository.endpoint)) {
|
||||
|
@ -380,10 +368,7 @@ export class CICheckRunPopover extends React.PureComponent<
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ci-check-run-list-container"
|
||||
style={this.getListHeightStyles()}
|
||||
>
|
||||
<div className="ci-check-run-list-container">
|
||||
<CICheckRunList
|
||||
checkRuns={checkRuns}
|
||||
loadingActionLogs={loadingActionLogs}
|
||||
|
@ -406,13 +391,16 @@ export class CICheckRunPopover extends React.PureComponent<
|
|||
return (
|
||||
<div className="ci-check-list-popover">
|
||||
<Popover
|
||||
anchor={this.props.anchor}
|
||||
anchorPosition={PopoverAnchorPosition.Bottom}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
ariaLabelledby="ci-check-run-header"
|
||||
caretPosition={PopoverCaretPosition.Top}
|
||||
onClickOutside={this.props.closePopover}
|
||||
style={this.getPopoverPositioningStyles()}
|
||||
>
|
||||
{this.renderHeader()}
|
||||
{this.renderList()}
|
||||
<div className="ci-check-run-list-wrapper">
|
||||
{this.renderHeader()}
|
||||
{this.renderList()}
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -37,7 +37,6 @@ export class CloneGenericRepository extends React.Component<
|
|||
placeholder="URL or username/repository"
|
||||
value={this.props.url}
|
||||
onValueChanged={this.onUrlChanged}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true}
|
||||
label={
|
||||
<span>
|
||||
|
|
|
@ -16,7 +16,7 @@ import * as OcticonSymbol from '../octicons/octicons.generated'
|
|||
export class DialogError extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<div className="dialog-error">
|
||||
<div className="dialog-error" role="alert">
|
||||
<Octicon symbol={OcticonSymbol.stop} />
|
||||
<div>{this.props.children}</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,11 @@ import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
|||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { RadioButton } from '../lib/radio-button'
|
||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchorPosition,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
|
||||
interface IDiffOptionsProps {
|
||||
readonly isInteractiveDiff: boolean
|
||||
|
@ -28,6 +32,7 @@ export class DiffOptions extends React.Component<
|
|||
IDiffOptionsState
|
||||
> {
|
||||
private diffOptionsRef = React.createRef<HTMLDivElement>()
|
||||
private gearIconRef = React.createRef<HTMLSpanElement>()
|
||||
|
||||
public constructor(props: IDiffOptionsProps) {
|
||||
super(props)
|
||||
|
@ -77,7 +82,9 @@ export class DiffOptions extends React.Component<
|
|||
return (
|
||||
<div className="diff-options-component" ref={this.diffOptionsRef}>
|
||||
<button onClick={this.onButtonClick}>
|
||||
<Octicon symbol={OcticonSymbol.gear} />
|
||||
<span ref={this.gearIconRef}>
|
||||
<Octicon symbol={OcticonSymbol.gear} />
|
||||
</span>
|
||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
||||
</button>
|
||||
{this.state.isPopoverOpen && this.renderPopover()}
|
||||
|
@ -89,7 +96,9 @@ export class DiffOptions extends React.Component<
|
|||
return (
|
||||
<Popover
|
||||
ariaLabelledby="diff-options-popover-header"
|
||||
caretPosition={PopoverCaretPosition.TopRight}
|
||||
anchor={this.gearIconRef.current}
|
||||
anchorPosition={PopoverAnchorPosition.BottomRight}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
onClickOutside={this.closePopover}
|
||||
>
|
||||
<h3 id="diff-options-popover-header">
|
||||
|
|
|
@ -34,7 +34,6 @@ export class DiffSearchInput extends React.Component<
|
|||
<TextBox
|
||||
placeholder="Search..."
|
||||
type="search"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true}
|
||||
onValueChanged={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
|
|
|
@ -82,6 +82,13 @@ interface IDiffProps {
|
|||
*/
|
||||
readonly onOpenBinaryFile: (fullPath: string) => void
|
||||
|
||||
/**
|
||||
* Callback to open a selected file using the configured external editor
|
||||
*
|
||||
* @param fullPath The full path to the file on disk
|
||||
*/
|
||||
readonly onOpenInExternalEditor?: (fullPath: string) => void
|
||||
|
||||
/** Called when the user requests to open a submodule. */
|
||||
readonly onOpenSubmodule?: (fullPath: string) => void
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { narrowNoNewlineSymbol } from './text-diff'
|
|||
import { shallowEquals, structuralEquals } from '../../lib/equality'
|
||||
import { DiffHunkExpansionType } from '../../models/diff'
|
||||
import { DiffExpansionKind } from './text-diff-expansion'
|
||||
import { PopoverCaretPosition } from '../lib/popover'
|
||||
import { PopoverAnchorPosition } from '../lib/popover'
|
||||
import { WhitespaceHintPopover } from './whitespace-hint-popover'
|
||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||
import { TooltipDirection } from '../lib/tooltip'
|
||||
|
@ -173,7 +173,10 @@ export class SideBySideDiffRow extends React.Component<
|
|||
return (
|
||||
<div className="row context">
|
||||
<div className="before">
|
||||
{this.renderLineNumbers([beforeLineNumber, afterLineNumber])}
|
||||
{this.renderLineNumbers(
|
||||
[beforeLineNumber, afterLineNumber],
|
||||
undefined
|
||||
)}
|
||||
{this.renderContentFromString(row.content, row.beforeTokens)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -183,11 +186,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
return (
|
||||
<div className="row context">
|
||||
<div className="before">
|
||||
{this.renderLineNumber(beforeLineNumber)}
|
||||
{this.renderLineNumber(beforeLineNumber, DiffColumn.Before)}
|
||||
{this.renderContentFromString(row.content, row.beforeTokens)}
|
||||
</div>
|
||||
<div className="after">
|
||||
{this.renderLineNumber(afterLineNumber)}
|
||||
{this.renderLineNumber(afterLineNumber, DiffColumn.After)}
|
||||
{this.renderContentFromString(row.content, row.afterTokens)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -202,7 +205,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
<div className={afterClasses}>
|
||||
{this.renderLineNumbers([undefined, lineNumber], isSelected)}
|
||||
{this.renderLineNumbers(
|
||||
[undefined, lineNumber],
|
||||
DiffColumn.After,
|
||||
isSelected
|
||||
)}
|
||||
{this.renderHunkHandle()}
|
||||
{this.renderContent(row.data)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.After)}
|
||||
|
@ -214,12 +221,12 @@ export class SideBySideDiffRow extends React.Component<
|
|||
return (
|
||||
<div className="row added" onMouseEnter={this.onMouseEnterLineNumber}>
|
||||
<div className={beforeClasses}>
|
||||
{this.renderLineNumber()}
|
||||
{this.renderLineNumber(undefined, DiffColumn.Before)}
|
||||
{this.renderContentFromString('')}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.Before)}
|
||||
</div>
|
||||
<div className={afterClasses}>
|
||||
{this.renderLineNumber(lineNumber, isSelected)}
|
||||
{this.renderLineNumber(lineNumber, DiffColumn.After, isSelected)}
|
||||
{this.renderContent(row.data)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.After)}
|
||||
</div>
|
||||
|
@ -236,7 +243,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
<div className={beforeClasses}>
|
||||
{this.renderLineNumbers([lineNumber, undefined], isSelected)}
|
||||
{this.renderLineNumbers(
|
||||
[lineNumber, undefined],
|
||||
DiffColumn.Before,
|
||||
isSelected
|
||||
)}
|
||||
{this.renderHunkHandle()}
|
||||
{this.renderContent(row.data)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.Before)}
|
||||
|
@ -251,12 +262,12 @@ export class SideBySideDiffRow extends React.Component<
|
|||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
<div className={beforeClasses}>
|
||||
{this.renderLineNumber(lineNumber, isSelected)}
|
||||
{this.renderLineNumber(lineNumber, DiffColumn.Before, isSelected)}
|
||||
{this.renderContent(row.data)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.Before)}
|
||||
</div>
|
||||
<div className={afterClasses}>
|
||||
{this.renderLineNumber()}
|
||||
{this.renderLineNumber(undefined, DiffColumn.After)}
|
||||
{this.renderContentFromString('')}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.After)}
|
||||
</div>
|
||||
|
@ -272,7 +283,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
className={beforeClasses}
|
||||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
{this.renderLineNumber(before.lineNumber, before.isSelected)}
|
||||
{this.renderLineNumber(
|
||||
before.lineNumber,
|
||||
DiffColumn.Before,
|
||||
before.isSelected
|
||||
)}
|
||||
{this.renderContent(before)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.Before)}
|
||||
</div>
|
||||
|
@ -280,7 +295,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
className={afterClasses}
|
||||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
{this.renderLineNumber(after.lineNumber, after.isSelected)}
|
||||
{this.renderLineNumber(
|
||||
after.lineNumber,
|
||||
DiffColumn.After,
|
||||
after.isSelected
|
||||
)}
|
||||
{this.renderContent(after)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.After)}
|
||||
</div>
|
||||
|
@ -455,21 +474,33 @@ export class SideBySideDiffRow extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private getLineNumbersContainerID(column: DiffColumn) {
|
||||
return `line-numbers-${this.props.numRow}-${column}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the line number box.
|
||||
*
|
||||
* @param lineNumbers Array with line numbers to display.
|
||||
* @param column Column to which the line number belongs.
|
||||
* @param isSelected Whether the line has been selected.
|
||||
* If undefined is passed, the line is treated
|
||||
* as non-selectable.
|
||||
*/
|
||||
private renderLineNumbers(
|
||||
lineNumbers: Array<number | undefined>,
|
||||
column: DiffColumn | undefined,
|
||||
isSelected?: boolean
|
||||
) {
|
||||
const wrapperID =
|
||||
column === undefined ? undefined : this.getLineNumbersContainerID(column)
|
||||
if (!this.props.isDiffSelectable || isSelected === undefined) {
|
||||
return (
|
||||
<div className="line-number" style={{ width: this.lineGutterWidth }}>
|
||||
<div
|
||||
id={wrapperID}
|
||||
className="line-number"
|
||||
style={{ width: this.lineGutterWidth }}
|
||||
>
|
||||
{lineNumbers.map((lineNumber, index) => (
|
||||
<span key={index}>{lineNumber}</span>
|
||||
))}
|
||||
|
@ -480,6 +511,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
id={wrapperID}
|
||||
className={classNames('line-number', 'selectable', 'hoverable', {
|
||||
'line-selected': isSelected,
|
||||
hover: this.props.isHunkHovered,
|
||||
|
@ -499,22 +531,21 @@ export class SideBySideDiffRow extends React.Component<
|
|||
if (this.state.showWhitespaceHint !== column) {
|
||||
return
|
||||
}
|
||||
|
||||
const caretPosition =
|
||||
column === DiffColumn.Before
|
||||
? PopoverCaretPosition.RightTop
|
||||
: PopoverCaretPosition.LeftTop
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
[column === DiffColumn.Before ? 'marginRight' : 'marginLeft']:
|
||||
this.lineGutterWidth + 10,
|
||||
marginTop: -10,
|
||||
const elementID = `line-numbers-${this.props.numRow}-${column}`
|
||||
const anchor = document.getElementById(elementID)
|
||||
if (anchor === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const anchorPosition =
|
||||
column === DiffColumn.Before
|
||||
? PopoverAnchorPosition.LeftTop
|
||||
: PopoverAnchorPosition.RightTop
|
||||
|
||||
return (
|
||||
<WhitespaceHintPopover
|
||||
caretPosition={caretPosition}
|
||||
style={style}
|
||||
anchor={anchor}
|
||||
anchorPosition={anchorPosition}
|
||||
onHideWhitespaceInDiffChanged={this.props.onHideWhitespaceInDiffChanged}
|
||||
onDismissed={this.onWhitespaceHintClose}
|
||||
/>
|
||||
|
@ -529,22 +560,21 @@ export class SideBySideDiffRow extends React.Component<
|
|||
* Renders the line number box.
|
||||
*
|
||||
* @param lineNumber Line number to display.
|
||||
* @param column Column to which the line number belongs.
|
||||
* @param isSelected Whether the line has been selected.
|
||||
* If undefined is passed, the line is treated
|
||||
* as non-selectable.
|
||||
*/
|
||||
private renderLineNumber(lineNumber?: number, isSelected?: boolean) {
|
||||
return this.renderLineNumbers([lineNumber], isSelected)
|
||||
private renderLineNumber(
|
||||
lineNumber: number | undefined,
|
||||
column: DiffColumn,
|
||||
isSelected?: boolean
|
||||
) {
|
||||
return this.renderLineNumbers([lineNumber], column, isSelected)
|
||||
}
|
||||
|
||||
private getDiffColumn(targetElement?: Element): DiffColumn | null {
|
||||
const { row, showSideBySideDiff } = this.props
|
||||
|
||||
// On unified diffs we don't have columns so we always use "before" to not
|
||||
// mess up with line selections.
|
||||
if (!showSideBySideDiff) {
|
||||
return DiffColumn.Before
|
||||
}
|
||||
const { row } = this.props
|
||||
|
||||
switch (row.type) {
|
||||
case DiffRowType.Added:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { Repository } from '../../models/repository'
|
||||
import {
|
||||
ITextDiff,
|
||||
|
|
|
@ -56,7 +56,7 @@ import {
|
|||
import { createOcticonElement } from '../octicons/octicon'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { WhitespaceHintPopover } from './whitespace-hint-popover'
|
||||
import { PopoverCaretPosition } from '../lib/popover'
|
||||
import { PopoverAnchorPosition } from '../lib/popover'
|
||||
import { HiddenBidiCharsWarning } from './hidden-bidi-chars-warning'
|
||||
|
||||
// This is a custom version of the no-newline octicon that's exactly as
|
||||
|
@ -982,7 +982,7 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
) {
|
||||
const marker = lineInfo.gutterMarkers[diffGutterName]
|
||||
if (marker instanceof HTMLElement) {
|
||||
this.updateGutterMarker(marker, hunk, diffLine)
|
||||
this.updateGutterMarker(lineInfo.line, marker, hunk, diffLine)
|
||||
}
|
||||
} else {
|
||||
batchedOps.push(() => {
|
||||
|
@ -1037,6 +1037,10 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
: canSelect(file) && file.selection.isSelected(index)
|
||||
}
|
||||
|
||||
private getGutterLineID(index: number) {
|
||||
return `diff-line-gutter-${index}`
|
||||
}
|
||||
|
||||
private getGutterLineClassNameInfo(
|
||||
hunk: DiffHunk,
|
||||
diffLine: DiffLine
|
||||
|
@ -1175,7 +1179,7 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
)
|
||||
}
|
||||
|
||||
this.updateGutterMarker(marker, hunk, diffLine)
|
||||
this.updateGutterMarker(index, marker, hunk, diffLine)
|
||||
|
||||
return marker
|
||||
}
|
||||
|
@ -1257,6 +1261,7 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
}
|
||||
|
||||
private updateGutterMarker(
|
||||
index: number,
|
||||
marker: HTMLElement,
|
||||
hunk: DiffHunk,
|
||||
diffLine: DiffLine
|
||||
|
@ -1270,6 +1275,8 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
}
|
||||
}
|
||||
|
||||
marker.id = this.getGutterLineID(index)
|
||||
|
||||
const hunkExpandWholeHandle = marker.getElementsByClassName(
|
||||
'hunk-expand-whole-handle'
|
||||
)[0]
|
||||
|
@ -1419,39 +1426,22 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
container.style.position = 'absolute'
|
||||
const scroller = cm.getScrollerElement()
|
||||
|
||||
const diffSize = getLineWidthFromDigitCount(
|
||||
getNumberOfDigits(this.state.diff.maxLineNumber)
|
||||
)
|
||||
|
||||
const lineY = cm.heightAtLine(index, 'local')
|
||||
// We're positioning relative to the scroll container, not the
|
||||
// sizer or lines so we'll have to account for the gutter width and
|
||||
// the hunk handle.
|
||||
const style: React.CSSProperties = { left: diffSize * 2 + 10 }
|
||||
let caretPosition = PopoverCaretPosition.LeftTop
|
||||
|
||||
// Offset down by 10px to align the popover arrow.
|
||||
container.style.top = `${lineY - 10}px`
|
||||
|
||||
// If the line is further than 50% down the viewport we'll flip the
|
||||
// popover to point upwards so as to not get hidden beneath (or above)
|
||||
// the scroll boundary.
|
||||
if (lineY - scroller.scrollTop > scroller.clientHeight / 2) {
|
||||
caretPosition = PopoverCaretPosition.LeftBottom
|
||||
style.bottom = -35
|
||||
}
|
||||
|
||||
scroller.appendChild(container)
|
||||
this.whitespaceHintContainer = container
|
||||
|
||||
ReactDOM.render(
|
||||
<WhitespaceHintPopover
|
||||
caretPosition={caretPosition}
|
||||
anchor={document.getElementById(this.getGutterLineID(index))}
|
||||
anchorPosition={PopoverAnchorPosition.RightTop}
|
||||
onDismissed={this.unmountWhitespaceHint}
|
||||
onHideWhitespaceInDiffChanged={
|
||||
this.props.onHideWhitespaceInDiffChanged
|
||||
}
|
||||
style={style}
|
||||
/>,
|
||||
container
|
||||
)
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverCaretPosition,
|
||||
PopoverAnchorPosition,
|
||||
PopoverAppearEffect,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
import { OkCancelButtonGroup } from '../dialog'
|
||||
|
||||
interface IWhitespaceHintPopoverProps {
|
||||
readonly caretPosition: PopoverCaretPosition
|
||||
readonly anchor: HTMLElement | null
|
||||
readonly anchorPosition: PopoverAnchorPosition
|
||||
/** Called when the user changes the hide whitespace in diffs setting. */
|
||||
readonly onHideWhitespaceInDiffChanged: (checked: boolean) => void
|
||||
readonly onDismissed: () => void
|
||||
readonly style: React.CSSProperties
|
||||
}
|
||||
|
||||
export class WhitespaceHintPopover extends React.Component<IWhitespaceHintPopoverProps> {
|
||||
public render() {
|
||||
return (
|
||||
<Popover
|
||||
caretPosition={this.props.caretPosition}
|
||||
anchor={this.props.anchor}
|
||||
anchorPosition={this.props.anchorPosition}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
onMousedownOutside={this.onDismissed}
|
||||
className={'whitespace-hint'}
|
||||
style={this.props.style}
|
||||
appearEffect={PopoverAppearEffect.Shake}
|
||||
ariaLabelledby="whitespace-hint-header"
|
||||
>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { RetryAction } from '../../models/retry-actions'
|
|||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { Ref } from '../lib/ref'
|
||||
import { LinkButton } from '../lib/link-button'
|
||||
import { PasswordTextBox } from '../lib/password-text-box'
|
||||
|
||||
interface IGenericGitAuthenticationProps {
|
||||
/** The hostname with which the user tried to authenticate. */
|
||||
|
@ -61,7 +62,6 @@ export class GenericGitAuthentication extends React.Component<
|
|||
<Row>
|
||||
<TextBox
|
||||
label="Username"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true}
|
||||
value={this.state.username}
|
||||
onValueChanged={this.onUsernameChange}
|
||||
|
@ -69,9 +69,8 @@ export class GenericGitAuthentication extends React.Component<
|
|||
</Row>
|
||||
|
||||
<Row>
|
||||
<TextBox
|
||||
<PasswordTextBox
|
||||
label="Password"
|
||||
type="password"
|
||||
value={this.state.password}
|
||||
onValueChanged={this.onPasswordChange}
|
||||
/>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { CommittedFileChange } from '../../models/status'
|
||||
import { List } from '../lib/list'
|
||||
import { ClickSource, List } from '../lib/list'
|
||||
import { CommittedFileItem } from './committed-file-item'
|
||||
|
||||
interface IFileListProps {
|
||||
readonly files: ReadonlyArray<CommittedFileChange>
|
||||
readonly selectedFile: CommittedFileChange | null
|
||||
readonly onSelectedFileChanged: (file: CommittedFileChange) => void
|
||||
readonly onRowDoubleClick: (row: number, source: ClickSource) => void
|
||||
readonly availableWidth: number
|
||||
readonly onContextMenu?: (
|
||||
file: CommittedFileChange,
|
||||
|
@ -47,6 +48,7 @@ export class FileList extends React.Component<IFileListProps> {
|
|||
rowHeight={29}
|
||||
selectedRows={[this.rowForFile(this.props.selectedFile)]}
|
||||
onSelectedRowChanged={this.onSelectedRowChanged}
|
||||
onRowDoubleClick={this.props.onRowDoubleClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -116,6 +116,13 @@ export class SelectedCommits extends React.Component<
|
|||
this.props.dispatcher.changeFileSelection(this.props.repository, file)
|
||||
}
|
||||
|
||||
private onRowDoubleClick = (row: number) => {
|
||||
const files = this.props.changesetData.files
|
||||
const file = files[row]
|
||||
|
||||
this.props.onOpenInExternalEditor(file.path)
|
||||
}
|
||||
|
||||
private onHistoryRef = (ref: HTMLDivElement | null) => {
|
||||
this.historyRef = ref
|
||||
}
|
||||
|
@ -252,6 +259,7 @@ export class SelectedCommits extends React.Component<
|
|||
selectedFile={this.props.selectedFile}
|
||||
availableWidth={availableWidth}
|
||||
onContextMenu={this.onContextMenu}
|
||||
onRowDoubleClick={this.onRowDoubleClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -389,7 +397,7 @@ export class SelectedCommits extends React.Component<
|
|||
},
|
||||
{
|
||||
label: openInExternalEditor,
|
||||
action: () => this.props.onOpenInExternalEditor(fullPath),
|
||||
action: () => this.props.onOpenInExternalEditor(file.path),
|
||||
enabled: fileExistsOnDisk,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -9,6 +9,7 @@ import { TextBox } from './text-box'
|
|||
import { Errors } from './errors'
|
||||
import { getDotComAPIEndpoint } from '../../lib/api'
|
||||
import { HorizontalRule } from './horizontal-rule'
|
||||
import { PasswordTextBox } from './password-text-box'
|
||||
|
||||
/** Text to let the user know their browser will send them back to GH Desktop */
|
||||
export const BrowserRedirectMessage =
|
||||
|
@ -107,14 +108,12 @@ export class AuthenticationForm extends React.Component<
|
|||
disabled={disabled}
|
||||
required={true}
|
||||
displayInvalidState={false}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true}
|
||||
autoFocus={this.props.endpoint === getDotComAPIEndpoint()}
|
||||
onValueChanged={this.onUsernameChange}
|
||||
/>
|
||||
|
||||
<TextBox
|
||||
<PasswordTextBox
|
||||
label="Password"
|
||||
type="password"
|
||||
disabled={disabled}
|
||||
required={true}
|
||||
displayInvalidState={false}
|
||||
|
@ -196,6 +195,7 @@ export class AuthenticationForm extends React.Component<
|
|||
type="submit"
|
||||
className="button-with-icon"
|
||||
onClick={this.signInWithBrowser}
|
||||
autoFocus={true}
|
||||
>
|
||||
Sign in using your browser
|
||||
<Octicon symbol={OcticonSymbol.linkExternal} />
|
||||
|
|
|
@ -187,7 +187,6 @@ export class AuthorInput extends React.Component<
|
|||
<AutocompletingInput<UserHit>
|
||||
elementId="author-input"
|
||||
placeholder="@username"
|
||||
isCombobox={true}
|
||||
alwaysAutocomplete={true}
|
||||
autocompletionProviders={[this.props.autoCompleteProvider]}
|
||||
autocompleteItemFilter={this.getAutocompleteItemFilter(
|
||||
|
|
|
@ -8,6 +8,42 @@ import { TooltippedContent } from './tooltipped-content'
|
|||
import { TooltipDirection } from './tooltip'
|
||||
import { supportsAvatarsAPI } from '../../lib/endpoint-capabilities'
|
||||
|
||||
/**
|
||||
* This maps contains avatar URLs that have failed to load and
|
||||
* the last time they failed to load (in milliseconds since the epoc)
|
||||
*
|
||||
* This is used to prevent us from retrying to load avatars where the
|
||||
* server returned an error (or was unreachable). Since browsers doesn't
|
||||
* cache the error itself and since we re-mount our image tags when
|
||||
* scrolling through our virtualized lists we can end up making a lot
|
||||
* of redundant requests to the server when it's busy or down. So
|
||||
* when an avatar fails to load we'll remember that and not attempt
|
||||
* to load it again for a while (see RetryLimit)
|
||||
*/
|
||||
const FailingAvatars = new Map<string, number>()
|
||||
|
||||
/**
|
||||
* Don't attempt to load an avatar that failed to load more than
|
||||
* once every 5 minutes
|
||||
*/
|
||||
const RetryLimit = 5 * 60 * 1000
|
||||
|
||||
function pruneExpiredFailingAvatars() {
|
||||
const expired = new Array<string>()
|
||||
|
||||
for (const [url, lastError] of FailingAvatars.entries()) {
|
||||
if (Date.now() - lastError > RetryLimit) {
|
||||
expired.push(url)
|
||||
} else {
|
||||
// Map is sorted by insertion order so we can bail out early assuming
|
||||
// we can trust the clock (which I know we can't but it's good enough)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
expired.forEach(url => FailingAvatars.delete(url))
|
||||
}
|
||||
|
||||
interface IAvatarProps {
|
||||
/** The user whose avatar should be displayed. */
|
||||
readonly user?: IAvatarUser
|
||||
|
@ -29,6 +65,7 @@ interface IAvatarProps {
|
|||
interface IAvatarState {
|
||||
readonly user?: IAvatarUser
|
||||
readonly candidates: ReadonlyArray<string>
|
||||
readonly imageLoaded: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -148,7 +185,7 @@ export class Avatar extends React.Component<IAvatarProps, IAvatarState> {
|
|||
const { user, size } = props
|
||||
if (!shallowEquals(user, state.user)) {
|
||||
const candidates = getAvatarUrlCandidates(user, size)
|
||||
return { user, candidates }
|
||||
return { user, candidates, imageLoaded: false }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@ -160,6 +197,7 @@ export class Avatar extends React.Component<IAvatarProps, IAvatarState> {
|
|||
this.state = {
|
||||
user,
|
||||
candidates: getAvatarUrlCandidates(user, size),
|
||||
imageLoaded: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,37 +233,32 @@ export class Avatar extends React.Component<IAvatarProps, IAvatarState> {
|
|||
}
|
||||
|
||||
private onImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
if (this.state.candidates.length > 0) {
|
||||
this.setState({ candidates: this.state.candidates.slice(1) })
|
||||
const { candidates } = this.state
|
||||
if (candidates.length > 0) {
|
||||
this.setState({
|
||||
candidates: candidates.filter(x => x !== e.currentTarget.src),
|
||||
imageLoaded: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
this.setState({ imageLoaded: true })
|
||||
}
|
||||
|
||||
public render() {
|
||||
const title = this.getTitle()
|
||||
const { user } = this.props
|
||||
const { imageLoaded } = this.state
|
||||
const alt = user
|
||||
? `Avatar for ${user.name || user.email}`
|
||||
: `Avatar for unknown user`
|
||||
|
||||
if (this.state.candidates.length === 0) {
|
||||
return (
|
||||
<Octicon
|
||||
symbol={DefaultAvatarSymbol}
|
||||
className="avatar"
|
||||
title={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const src = this.state.candidates[0]
|
||||
|
||||
const img = (
|
||||
<img className="avatar" src={src} alt={alt} onError={this.onImageError} />
|
||||
)
|
||||
|
||||
if (title === undefined) {
|
||||
return img
|
||||
}
|
||||
const now = Date.now()
|
||||
const src = this.state.candidates.find(c => {
|
||||
const lastFailed = FailingAvatars.get(c)
|
||||
return lastFailed === undefined || now - lastFailed > RetryLimit
|
||||
})
|
||||
|
||||
return (
|
||||
<TooltippedContent
|
||||
|
@ -235,13 +268,43 @@ export class Avatar extends React.Component<IAvatarProps, IAvatarState> {
|
|||
direction={TooltipDirection.NORTH}
|
||||
tagName="div"
|
||||
>
|
||||
{img}
|
||||
{!imageLoaded && (
|
||||
<Octicon symbol={DefaultAvatarSymbol} className="avatar" />
|
||||
)}
|
||||
{src && (
|
||||
<img
|
||||
className="avatar"
|
||||
// This is critical for the functionality of onImageRef, we need a
|
||||
// new Image element for each unique url.
|
||||
key={src}
|
||||
ref={this.onImageRef}
|
||||
src={src}
|
||||
alt={alt}
|
||||
onLoad={this.onImageLoad}
|
||||
onError={this.onImageError}
|
||||
style={{ display: imageLoaded ? undefined : 'none' }}
|
||||
/>
|
||||
)}
|
||||
</TooltippedContent>
|
||||
)
|
||||
}
|
||||
|
||||
private onImageRef = (img: HTMLImageElement | null) => {
|
||||
// This is different from the onImageLoad react event handler because we're
|
||||
// never unsubscribing from this. If we were to use the react event handler
|
||||
// we'd miss errors that happen after the Avatar component (or img
|
||||
// component) has unmounted. We use a `key` on the img element to ensure
|
||||
// we're always using a new img element for each unique url.
|
||||
img?.addEventListener('error', () => {
|
||||
// Keep the map sorted on last failure, see pruneExpiredFailingAvatars
|
||||
FailingAvatars.delete(img.src)
|
||||
FailingAvatars.set(img.src, Date.now())
|
||||
})
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener('online', this.onInternetConnected)
|
||||
pruneExpiredFailingAvatars()
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -249,6 +312,10 @@ export class Avatar extends React.Component<IAvatarProps, IAvatarState> {
|
|||
}
|
||||
|
||||
private onInternetConnected = () => {
|
||||
// Let's assume us being offline was the reason for failing to
|
||||
// load the avatars
|
||||
FailingAvatars.clear()
|
||||
|
||||
// If we've been offline and therefore failed to load an avatar
|
||||
// we'll automatically retry when the user becomes connected again.
|
||||
if (this.state.candidates.length === 0) {
|
||||
|
|
|
@ -122,6 +122,18 @@ export interface IButtonProps {
|
|||
* bounds. Typically this is used in conjunction with an ellipsis CSS ruleset.
|
||||
*/
|
||||
readonly onlyShowTooltipWhenOverflowed?: boolean
|
||||
|
||||
/** The aria-pressed attribute indicates the current "pressed" state of a
|
||||
* toggle button.
|
||||
*
|
||||
* Accessibility notes: Do not change the contents of the label on a toggle
|
||||
* button when the state changes. If a button label says "pause", do not
|
||||
* change it to "play" when pressed.
|
||||
* */
|
||||
readonly ariaPressed?: boolean
|
||||
|
||||
/** Whether the input field should auto focus when mounted. */
|
||||
readonly autoFocus?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,6 +188,8 @@ export class Button extends React.Component<IButtonProps, {}> {
|
|||
aria-disabled={disabled ? 'true' : undefined}
|
||||
aria-label={this.props.ariaLabel}
|
||||
aria-haspopup={this.props.ariaHaspopup}
|
||||
aria-pressed={this.props.ariaPressed}
|
||||
autoFocus={this.props.autoFocus}
|
||||
>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
|
|
|
@ -21,6 +21,7 @@ import { RadioButton } from './radio-button'
|
|||
import { Select } from './select'
|
||||
import { GitEmailNotFoundWarning } from './git-email-not-found-warning'
|
||||
import { getDotComAPIEndpoint } from '../../lib/api'
|
||||
import { Loading } from './loading'
|
||||
|
||||
interface IConfigureGitUserProps {
|
||||
/** The logged-in accounts. */
|
||||
|
@ -53,6 +54,8 @@ interface IConfigureGitUserState {
|
|||
* choice to delete the lock file.
|
||||
*/
|
||||
readonly existingLockFilePath?: string
|
||||
|
||||
readonly loadingGitConfig: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,6 +85,7 @@ export class ConfigureGitUser extends React.Component<
|
|||
gitHubName: account?.name || account?.login || '',
|
||||
gitHubEmail:
|
||||
this.account !== null ? lookupPreferredEmail(this.account) : '',
|
||||
loadingGitConfig: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,6 +117,7 @@ export class ConfigureGitUser extends React.Component<
|
|||
prevState.manualEmail.length === 0
|
||||
? globalUserEmail || ''
|
||||
: prevState.manualEmail,
|
||||
loadingGitConfig: false,
|
||||
}),
|
||||
() => {
|
||||
// Chances are low that we actually have an account at mount-time
|
||||
|
@ -276,6 +281,7 @@ export class ConfigureGitUser extends React.Component<
|
|||
checked={this.state.useGitHubAuthorInfo}
|
||||
onSelected={this.onUseGitHubInfoSelected}
|
||||
value="github-account"
|
||||
autoFocus={true}
|
||||
/>
|
||||
<RadioButton
|
||||
label="Configure manually"
|
||||
|
@ -298,7 +304,7 @@ export class ConfigureGitUser extends React.Component<
|
|||
label="Name"
|
||||
placeholder="Your Name"
|
||||
value={this.state.gitHubName}
|
||||
disabled={true}
|
||||
readOnly={true}
|
||||
/>
|
||||
|
||||
<Select
|
||||
|
@ -324,20 +330,30 @@ export class ConfigureGitUser extends React.Component<
|
|||
private renderGitConfigForm() {
|
||||
return (
|
||||
<Form className="sign-in-form" onSubmit={this.save}>
|
||||
<TextBox
|
||||
label="Name"
|
||||
placeholder="Your Name"
|
||||
value={this.state.manualName}
|
||||
onValueChanged={this.onNameChange}
|
||||
/>
|
||||
{this.state.loadingGitConfig && (
|
||||
<div className="git-config-loading">
|
||||
<Loading /> Checking for an existing git config…
|
||||
</div>
|
||||
)}
|
||||
{!this.state.loadingGitConfig && (
|
||||
<>
|
||||
<TextBox
|
||||
label="Name"
|
||||
placeholder="Your Name"
|
||||
onValueChanged={this.onNameChange}
|
||||
value={this.state.manualName}
|
||||
autoFocus={true}
|
||||
/>
|
||||
|
||||
<TextBox
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="your-email@example.com"
|
||||
value={this.state.manualEmail}
|
||||
onValueChanged={this.onEmailChange}
|
||||
/>
|
||||
<TextBox
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="your-email@example.com"
|
||||
value={this.state.manualEmail}
|
||||
onValueChanged={this.onEmailChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{this.account !== null && (
|
||||
<GitEmailNotFoundWarning
|
||||
|
|
|
@ -55,7 +55,6 @@ export class EnterpriseServerEntry extends React.Component<
|
|||
<Form onSubmit={this.onSubmit}>
|
||||
<TextBox
|
||||
label="Enterprise or AE address"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true}
|
||||
disabled={disableEntry}
|
||||
onValueChanged={this.onServerAddressChanged}
|
||||
|
|
|
@ -41,7 +41,6 @@ export class FancyTextBox extends React.Component<
|
|||
value={this.props.value}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={this.props.autoFocus}
|
||||
disabled={this.props.disabled}
|
||||
type={this.props.type}
|
||||
|
|
|
@ -12,6 +12,7 @@ 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'
|
||||
|
||||
/** An item in the filter list. */
|
||||
export interface IFilterListItem {
|
||||
|
@ -154,11 +155,24 @@ interface IFilterListProps<T extends IFilterListItem> {
|
|||
|
||||
/** 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<IFilterListRow<T>>
|
||||
readonly selectedRow: number
|
||||
readonly filterValue: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -250,7 +264,6 @@ export class FilterList<T extends IFilterListItem> extends React.Component<
|
|||
<TextBox
|
||||
ref={this.onTextBoxRef}
|
||||
type="search"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true}
|
||||
placeholder={this.props.placeholderText || 'Filter'}
|
||||
className="filter-list-filter-field"
|
||||
|
@ -277,8 +290,14 @@ export class FilterList<T extends IFilterListItem> extends React.Component<
|
|||
}
|
||||
|
||||
public render() {
|
||||
const itemRows = this.state.rows.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()}
|
||||
|
@ -343,6 +362,7 @@ export class FilterList<T extends IFilterListItem> extends React.Component<
|
|||
onSelectedRowChanged={this.onSelectedRowChanged}
|
||||
onRowClick={this.onRowClick}
|
||||
onRowKeyDown={this.onRowKeyDown}
|
||||
onRowContextMenu={this.onRowContextMenu}
|
||||
canSelectRow={this.canSelectRow}
|
||||
invalidationProps={{
|
||||
...this.props,
|
||||
|
@ -419,6 +439,23 @@ export class FilterList<T extends IFilterListItem> extends React.Component<
|
|||
}
|
||||
}
|
||||
|
||||
private onRowContextMenu = (
|
||||
index: number,
|
||||
source: React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (!this.props.onItemContextMenu) {
|
||||
return
|
||||
}
|
||||
|
||||
const row = this.state.rows[index]
|
||||
|
||||
if (row.kind !== 'item') {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.onItemContextMenu(row.item, source)
|
||||
}
|
||||
|
||||
private onRowKeyDown = (row: number, event: React.KeyboardEvent<any>) => {
|
||||
const list = this.list
|
||||
if (!list) {
|
||||
|
@ -577,7 +614,7 @@ function createStateUpdate<T extends IFilterListItem>(
|
|||
selectedRow = flattenedRows.findIndex(i => i.kind === 'item')
|
||||
}
|
||||
|
||||
return { rows: flattenedRows, selectedRow }
|
||||
return { rows: flattenedRows, selectedRow, filterValue: filter }
|
||||
}
|
||||
|
||||
function getItemFromRowIndex<T extends IFilterListItem>(
|
||||
|
|
|
@ -18,6 +18,8 @@ interface IGitConfigUserFormProps {
|
|||
|
||||
readonly onNameChanged: (name: string) => void
|
||||
readonly onEmailChanged: (email: string) => void
|
||||
|
||||
readonly isLoadingGitConfig: boolean
|
||||
}
|
||||
|
||||
interface IGitConfigUserFormState {
|
||||
|
@ -49,7 +51,8 @@ export class GitConfigUserForm extends React.Component<
|
|||
this.state = {
|
||||
emailIsOther:
|
||||
this.accountEmails.length > 0 &&
|
||||
!this.accountEmails.includes(this.props.email),
|
||||
!this.accountEmails.includes(this.props.email) &&
|
||||
!this.props.isLoadingGitConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,7 +74,8 @@ export class GitConfigUserForm extends React.Component<
|
|||
this.setState({
|
||||
emailIsOther:
|
||||
this.accountEmails.length > 0 &&
|
||||
!this.accountEmails.includes(this.props.email),
|
||||
!this.accountEmails.includes(this.props.email) &&
|
||||
!this.props.isLoadingGitConfig,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { getDotComAPIEndpoint } from '../../lib/api'
|
|||
import { isAttributableEmailFor } from '../../lib/email'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { debounce } from 'lodash'
|
||||
import { AriaLiveContainer } from '../accessibility/aria-live-container'
|
||||
|
||||
interface IGitEmailNotFoundWarningProps {
|
||||
/** The account the commit should be attributed to. */
|
||||
|
@ -15,41 +15,11 @@ interface IGitEmailNotFoundWarningProps {
|
|||
readonly email: string
|
||||
}
|
||||
|
||||
interface IGitEmailNotFoundWarningState {
|
||||
/** The generated message debounced for the screen reader */
|
||||
readonly debouncedMessage: JSX.Element | null
|
||||
}
|
||||
|
||||
/**
|
||||
* A component which just displays a warning to the user if their git config
|
||||
* email doesn't match any of the emails in their GitHub (Enterprise) account.
|
||||
*/
|
||||
export class GitEmailNotFoundWarning extends React.Component<
|
||||
IGitEmailNotFoundWarningProps,
|
||||
IGitEmailNotFoundWarningState
|
||||
> {
|
||||
private onEmailChanged = debounce((message: JSX.Element | null) => {
|
||||
this.setState({ debouncedMessage: message })
|
||||
}, 1000)
|
||||
|
||||
public constructor(props: IGitEmailNotFoundWarningProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
debouncedMessage: this.buildMessage(),
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IGitEmailNotFoundWarningProps) {
|
||||
if (prevProps.email !== this.props.email) {
|
||||
this.onEmailChanged(this.buildMessage())
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.onEmailChanged.cancel()
|
||||
}
|
||||
|
||||
export class GitEmailNotFoundWarning extends React.Component<IGitEmailNotFoundWarningProps> {
|
||||
private buildMessage() {
|
||||
const { accounts, email } = this.props
|
||||
|
||||
|
@ -93,7 +63,6 @@ export class GitEmailNotFoundWarning extends React.Component<
|
|||
|
||||
public render() {
|
||||
const { accounts, email } = this.props
|
||||
const { debouncedMessage } = this.state
|
||||
|
||||
if (accounts.length === 0 || email.trim().length === 0) {
|
||||
return null
|
||||
|
@ -108,14 +77,12 @@ export class GitEmailNotFoundWarning extends React.Component<
|
|||
<>
|
||||
<div className="git-email-not-found-warning">{this.buildMessage()}</div>
|
||||
|
||||
<div
|
||||
<AriaLiveContainer
|
||||
id="git-email-not-found-warning-for-screen-readers"
|
||||
className="sr-only"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
trackedUserInput={this.props.email}
|
||||
>
|
||||
{debouncedMessage}
|
||||
</div>
|
||||
{this.buildMessage()}
|
||||
</AriaLiveContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,9 @@ interface IListRowProps {
|
|||
/** callback to fire when the row is clicked */
|
||||
readonly onRowClick: (index: number, e: React.MouseEvent<any>) => void
|
||||
|
||||
/** callback to fire when the row is double clicked */
|
||||
readonly onRowDoubleClick: (index: number, e: React.MouseEvent<any>) => void
|
||||
|
||||
/** callback to fire when the row receives a keyboard event */
|
||||
readonly onRowKeyDown: (index: number, e: React.KeyboardEvent<any>) => void
|
||||
|
||||
|
@ -82,6 +85,10 @@ export class ListRow extends React.Component<IListRowProps, {}> {
|
|||
this.props.onRowClick(this.props.rowIndex, e)
|
||||
}
|
||||
|
||||
private onRowDoubleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.props.onRowDoubleClick(this.props.rowIndex, e)
|
||||
}
|
||||
|
||||
private onRowKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
this.props.onRowKeyDown(this.props.rowIndex, e)
|
||||
}
|
||||
|
@ -128,6 +135,7 @@ export class ListRow extends React.Component<IListRowProps, {}> {
|
|||
onMouseDown={this.onRowMouseDown}
|
||||
onMouseUp={this.onRowMouseUp}
|
||||
onClick={this.onRowClick}
|
||||
onDoubleClick={this.onRowDoubleClick}
|
||||
onKeyDown={this.onRowKeyDown}
|
||||
style={style}
|
||||
onFocus={this.onFocus}
|
||||
|
|
|
@ -113,6 +113,8 @@ interface IListProps {
|
|||
*/
|
||||
readonly onRowClick?: (row: number, source: ClickSource) => void
|
||||
|
||||
readonly onRowDoubleClick?: (row: number, source: IMouseClickSource) => void
|
||||
|
||||
/**
|
||||
* This prop defines the behaviour of the selection of items within this list.
|
||||
* - 'single' : (default) single list-item selection. [shift] and [ctrl] have
|
||||
|
@ -943,6 +945,7 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
rowIndex={rowIndex}
|
||||
selected={selected}
|
||||
onRowClick={this.onRowClick}
|
||||
onRowDoubleClick={this.onRowDoubleClick}
|
||||
onRowKeyDown={this.onRowKeyDown}
|
||||
onRowMouseDown={this.onRowMouseDown}
|
||||
onRowMouseUp={this.onRowMouseUp}
|
||||
|
@ -1300,6 +1303,14 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onRowDoubleClick = (row: number, event: React.MouseEvent<any>) => {
|
||||
if (!this.props.onRowDoubleClick) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.onRowDoubleClick(row, { kind: 'mouseclick', event })
|
||||
}
|
||||
|
||||
private onScroll = ({
|
||||
scrollTop,
|
||||
clientHeight,
|
||||
|
|
53
app/src/ui/lib/password-text-box.tsx
Normal file
53
app/src/ui/lib/password-text-box.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import * as React from 'react'
|
||||
import { ITextBoxProps, TextBox } from './text-box'
|
||||
import { Button } from './button'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
|
||||
interface IPasswordTextBoxState {
|
||||
/**
|
||||
* Whether or not the password is currently visible in the underlying input
|
||||
*/
|
||||
readonly showPassword: boolean
|
||||
}
|
||||
|
||||
/** An password input element with app-standard styles and a button for toggling
|
||||
* the visibility of the user password. */
|
||||
export class PasswordTextBox extends React.Component<
|
||||
ITextBoxProps,
|
||||
IPasswordTextBoxState
|
||||
> {
|
||||
private textBoxRef = React.createRef<TextBox>()
|
||||
|
||||
public constructor(props: ITextBoxProps) {
|
||||
super(props)
|
||||
|
||||
this.state = { showPassword: false }
|
||||
}
|
||||
|
||||
private onTogglePasswordVisibility = () => {
|
||||
this.setState({ showPassword: !this.state.showPassword })
|
||||
this.textBoxRef.current!.focus()
|
||||
}
|
||||
|
||||
public render() {
|
||||
const buttonIcon = this.state.showPassword
|
||||
? OcticonSymbol.eye
|
||||
: OcticonSymbol.eyeClosed
|
||||
const type = this.state.showPassword ? 'text' : 'password'
|
||||
const props: ITextBoxProps = { ...this.props, ...{ type } }
|
||||
return (
|
||||
<div className="password-text-box">
|
||||
<TextBox {...props} ref={this.textBoxRef} />
|
||||
<Button
|
||||
ariaLabel="Toggle password visibility"
|
||||
tooltip="Toggle password visibility"
|
||||
onClick={this.onTogglePasswordVisibility}
|
||||
ariaPressed={this.state.showPassword}
|
||||
>
|
||||
<Octicon symbol={buttonIcon} />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
import * as React from 'react'
|
||||
import { Button } from './button'
|
||||
import { Popover, PopoverCaretPosition } from './popover'
|
||||
import { Popover, PopoverAnchorPosition, PopoverDecoration } from './popover'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const defaultPopoverContentHeight = 300
|
||||
const maxPopoverContentHeight = 500
|
||||
|
||||
interface IPopoverDropdownProps {
|
||||
|
@ -17,7 +16,6 @@ interface IPopoverDropdownProps {
|
|||
|
||||
interface IPopoverDropdownState {
|
||||
readonly showPopover: boolean
|
||||
readonly popoverContentHeight: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,36 +33,6 @@ export class PopoverDropdown extends React.Component<
|
|||
|
||||
this.state = {
|
||||
showPopover: false,
|
||||
popoverContentHeight: defaultPopoverContentHeight,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.calculateDropdownListHeight()
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.calculateDropdownListHeight()
|
||||
}
|
||||
|
||||
private calculateDropdownListHeight = () => {
|
||||
if (this.invokeButtonRef === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const windowHeight = window.innerHeight
|
||||
const bottomOfButton = this.invokeButtonRef.getBoundingClientRect().bottom
|
||||
const listHeaderHeight = 75
|
||||
const calcMaxHeight = Math.round(
|
||||
windowHeight - bottomOfButton - listHeaderHeight
|
||||
)
|
||||
|
||||
const popoverContentHeight =
|
||||
calcMaxHeight > maxPopoverContentHeight
|
||||
? maxPopoverContentHeight
|
||||
: calcMaxHeight
|
||||
if (popoverContentHeight !== this.state.popoverContentHeight) {
|
||||
this.setState({ popoverContentHeight })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,29 +54,30 @@ export class PopoverDropdown extends React.Component<
|
|||
}
|
||||
|
||||
const { contentTitle } = this.props
|
||||
const { popoverContentHeight } = this.state
|
||||
const contentStyle = { height: `${popoverContentHeight}px` }
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="popover-dropdown-popover"
|
||||
caretPosition={PopoverCaretPosition.TopLeft}
|
||||
anchor={this.invokeButtonRef}
|
||||
anchorPosition={PopoverAnchorPosition.BottomLeft}
|
||||
maxHeight={maxPopoverContentHeight}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
onClickOutside={this.closePopover}
|
||||
aria-labelledby="popover-dropdown-header"
|
||||
>
|
||||
<div className="popover-dropdown-header">
|
||||
<span id="popover-dropdown-header">{contentTitle}</span>
|
||||
<div className="popover-dropdown-wrapper">
|
||||
<div className="popover-dropdown-header">
|
||||
<span id="popover-dropdown-header">{contentTitle}</span>
|
||||
|
||||
<button
|
||||
className="close"
|
||||
onClick={this.closePopover}
|
||||
aria-label="close"
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.x} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="popover-dropdown-content" style={contentStyle}>
|
||||
{this.props.children}
|
||||
<button
|
||||
className="close"
|
||||
onClick={this.closePopover}
|
||||
aria-label="close"
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.x} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="popover-dropdown-content">{this.props.children}</div>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
|
|
|
@ -2,49 +2,94 @@ import * as React from 'react'
|
|||
import FocusTrap from 'focus-trap-react'
|
||||
import { Options as FocusTrapOptions } from 'focus-trap'
|
||||
import classNames from 'classnames'
|
||||
import {
|
||||
ComputePositionReturn,
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
} from '@floating-ui/react-dom'
|
||||
import {
|
||||
arrow,
|
||||
flip,
|
||||
offset,
|
||||
Placement,
|
||||
shift,
|
||||
Side,
|
||||
size,
|
||||
} from '@floating-ui/core'
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
|
||||
/**
|
||||
* Position of the caret relative to the pop up. It's composed by 2 dimensions:
|
||||
* - The first one is the edge on which the caret will rest.
|
||||
* - The second one is the alignment of the caret within that edge.
|
||||
* Position of the popover relative to its anchor element. It's composed by 2
|
||||
* dimensions:
|
||||
* - The first one is the edge of the anchor element from which the popover will
|
||||
* be displayed.
|
||||
* - The second one is the alignment of the popover within that edge.
|
||||
*
|
||||
* Example: TopRight means the caret will be in the top edge, on its right side.
|
||||
*
|
||||
* **Note:** If new positions are added to this enum, the value given to them
|
||||
* is prepended with `popover-caret-` to create a class name which defines, in
|
||||
* `app/styles/ui/_popover.scss`, where the caret is located for that specific
|
||||
* position.
|
||||
* Example: BottomRight means the popover will be in the bottom edge of the
|
||||
* anchor element, on its right side.
|
||||
**/
|
||||
export enum PopoverCaretPosition {
|
||||
export enum PopoverAnchorPosition {
|
||||
Top = 'top',
|
||||
TopRight = 'top-right',
|
||||
TopLeft = 'top-left',
|
||||
Left = 'left',
|
||||
LeftTop = 'left-top',
|
||||
LeftBottom = 'left-bottom',
|
||||
Bottom = 'bottom',
|
||||
BottomLeft = 'bottom-left',
|
||||
BottomRight = 'bottom-right',
|
||||
Right = 'right',
|
||||
RightTop = 'right-top',
|
||||
None = 'none',
|
||||
RightBottom = 'right-bottom',
|
||||
}
|
||||
|
||||
export enum PopoverAppearEffect {
|
||||
Shake = 'shake',
|
||||
}
|
||||
|
||||
export enum PopoverDecoration {
|
||||
None = 'none',
|
||||
Balloon = 'balloon',
|
||||
}
|
||||
|
||||
const TipSize = 8
|
||||
const TipCornerPadding = TipSize
|
||||
const ScreenBorderPadding = 10
|
||||
|
||||
interface IPopoverProps {
|
||||
readonly onClickOutside?: (event?: MouseEvent) => void
|
||||
readonly onMousedownOutside?: (event?: MouseEvent) => void
|
||||
/** The position of the caret or pointer towards the content to which the the
|
||||
* popover refers. If the caret position is not provided, the popup will have
|
||||
* no caret. */
|
||||
readonly caretPosition?: PopoverCaretPosition
|
||||
/** Element to anchor the popover to */
|
||||
readonly anchor: HTMLElement | null
|
||||
/** The position of the popover relative to the anchor. */
|
||||
readonly anchorPosition: PopoverAnchorPosition
|
||||
/**
|
||||
* The position of the tip or pointer of the popover relative to the side at
|
||||
* which the tip is presented. Optional. Default: Center
|
||||
*/
|
||||
readonly className?: string
|
||||
readonly style?: React.CSSProperties
|
||||
readonly appearEffect?: PopoverAppearEffect
|
||||
readonly ariaLabelledby?: string
|
||||
readonly trapFocus?: boolean // Default: true
|
||||
readonly decoration?: PopoverDecoration // Default: none
|
||||
|
||||
/** Maximum height decided by clients of Popover */
|
||||
readonly maxHeight?: number
|
||||
/** Minimum height decided by clients of Popover */
|
||||
readonly minHeight?: number
|
||||
}
|
||||
|
||||
export class Popover extends React.Component<IPopoverProps> {
|
||||
interface IPopoverState {
|
||||
readonly position: ComputePositionReturn | null
|
||||
}
|
||||
|
||||
export class Popover extends React.Component<IPopoverProps, IPopoverState> {
|
||||
private focusTrapOptions: FocusTrapOptions
|
||||
public containerDivRef = React.createRef<HTMLDivElement>()
|
||||
private containerDivRef = React.createRef<HTMLDivElement>()
|
||||
private contentDivRef = React.createRef<HTMLDivElement>()
|
||||
private tipDivRef = React.createRef<HTMLDivElement>()
|
||||
private floatingCleanUp: (() => void) | null = null
|
||||
|
||||
public constructor(props: IPopoverProps) {
|
||||
super(props)
|
||||
|
@ -54,11 +99,88 @@ export class Popover extends React.Component<IPopoverProps> {
|
|||
escapeDeactivates: true,
|
||||
onDeactivate: this.props.onClickOutside,
|
||||
}
|
||||
|
||||
this.state = { position: null }
|
||||
}
|
||||
|
||||
private async setupPosition() {
|
||||
this.floatingCleanUp?.()
|
||||
this.floatingCleanUp = null
|
||||
|
||||
const { anchor } = this.props
|
||||
|
||||
if (
|
||||
anchor === null ||
|
||||
anchor === undefined ||
|
||||
this.containerDivRef.current === null
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.floatingCleanUp = autoUpdate(
|
||||
anchor,
|
||||
this.containerDivRef.current,
|
||||
this.updatePosition
|
||||
)
|
||||
}
|
||||
|
||||
private updatePosition = async () => {
|
||||
const { anchor, decoration, maxHeight } = this.props
|
||||
const containerDiv = this.containerDivRef.current
|
||||
const contentDiv = this.contentDivRef.current
|
||||
|
||||
if (
|
||||
anchor === null ||
|
||||
anchor === undefined ||
|
||||
containerDiv === null ||
|
||||
contentDiv === null
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const tipDiv = this.tipDivRef.current
|
||||
|
||||
const middleware = [
|
||||
offset(decoration === PopoverDecoration.Balloon ? TipSize : 0),
|
||||
shift({ padding: ScreenBorderPadding }),
|
||||
flip({ padding: ScreenBorderPadding }),
|
||||
size({
|
||||
apply({ availableHeight, availableWidth }) {
|
||||
Object.assign(contentDiv.style, {
|
||||
maxHeight:
|
||||
maxHeight === undefined
|
||||
? `${availableHeight}px`
|
||||
: `${Math.min(availableHeight, maxHeight)}px`,
|
||||
maxWidth: `${availableWidth}px`,
|
||||
})
|
||||
},
|
||||
padding: ScreenBorderPadding,
|
||||
}),
|
||||
]
|
||||
|
||||
if (decoration === PopoverDecoration.Balloon && tipDiv) {
|
||||
middleware.push(arrow({ element: tipDiv, padding: TipCornerPadding }))
|
||||
}
|
||||
|
||||
const position = await computePosition(anchor, containerDiv, {
|
||||
strategy: 'fixed',
|
||||
placement: this.getFloatingPlacementForAnchorPosition(),
|
||||
middleware,
|
||||
})
|
||||
|
||||
this.setState({ position })
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
document.addEventListener('click', this.onDocumentClick)
|
||||
document.addEventListener('mousedown', this.onDocumentMouseDown)
|
||||
this.setupPosition()
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IPopoverProps) {
|
||||
if (prevProps.anchor !== this.props.anchor) {
|
||||
this.setupPosition()
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -67,7 +189,7 @@ export class Popover extends React.Component<IPopoverProps> {
|
|||
}
|
||||
|
||||
private onDocumentClick = (event: MouseEvent) => {
|
||||
const { current: ref } = this.containerDivRef
|
||||
const ref = this.containerDivRef.current
|
||||
const { target } = event
|
||||
|
||||
if (
|
||||
|
@ -82,7 +204,7 @@ export class Popover extends React.Component<IPopoverProps> {
|
|||
}
|
||||
|
||||
private onDocumentMouseDown = (event: MouseEvent) => {
|
||||
const { current: ref } = this.containerDivRef
|
||||
const ref = this.containerDivRef.current
|
||||
const { target } = event
|
||||
|
||||
if (
|
||||
|
@ -97,31 +219,163 @@ export class Popover extends React.Component<IPopoverProps> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
trapFocus,
|
||||
className,
|
||||
appearEffect,
|
||||
ariaLabelledby,
|
||||
children,
|
||||
decoration,
|
||||
maxHeight,
|
||||
minHeight,
|
||||
} = this.props
|
||||
const cn = classNames(
|
||||
'popover-component',
|
||||
this.getClassNameForCaret(),
|
||||
this.props.className,
|
||||
this.props.appearEffect && `appear-${this.props.appearEffect}`
|
||||
decoration === PopoverDecoration.Balloon && 'popover-component',
|
||||
className,
|
||||
appearEffect && `appear-${appearEffect}`
|
||||
)
|
||||
|
||||
return (
|
||||
<FocusTrap active={true} focusTrapOptions={this.focusTrapOptions}>
|
||||
const { position } = this.state
|
||||
// Make sure the popover *always* has at least `position: fixed` set, otherwise
|
||||
// it can cause weird layout glitches.
|
||||
const style: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
zIndex: 17, // same as --foldout-z-index
|
||||
height: 'auto',
|
||||
}
|
||||
const contentStyle: React.CSSProperties = {
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}
|
||||
let tipStyle: React.CSSProperties = {}
|
||||
|
||||
if (position) {
|
||||
style.top = position.y === undefined ? undefined : `${position.y}px`
|
||||
style.left = position.x === undefined ? undefined : `${position.x}px`
|
||||
contentStyle.minHeight =
|
||||
minHeight === undefined ? undefined : `${minHeight}px`
|
||||
contentStyle.height =
|
||||
maxHeight === undefined ? undefined : `${maxHeight}px`
|
||||
|
||||
const arrow = position.middlewareData.arrow
|
||||
|
||||
if (arrow) {
|
||||
const side: Side = position.placement.split('-')[0] as Side
|
||||
|
||||
const staticSide = {
|
||||
top: 'bottom',
|
||||
right: 'left',
|
||||
bottom: 'top',
|
||||
left: 'right',
|
||||
}[side]
|
||||
|
||||
const angle = {
|
||||
top: '270deg',
|
||||
right: '0deg',
|
||||
bottom: '90deg',
|
||||
left: '180deg',
|
||||
}[side]
|
||||
|
||||
tipStyle = {
|
||||
top: arrow.y,
|
||||
left: arrow.x,
|
||||
transform: `rotate(${angle})`,
|
||||
[staticSide]: this.tipDivRef.current
|
||||
? `${-this.tipDivRef.current.offsetWidth}px`
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={cn}
|
||||
style={style}
|
||||
ref={this.containerDivRef}
|
||||
aria-labelledby={ariaLabelledby}
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
className={cn}
|
||||
ref={this.containerDivRef}
|
||||
style={this.props.style}
|
||||
aria-labelledby={this.props.ariaLabelledby}
|
||||
role="dialog"
|
||||
className="popover-content"
|
||||
style={contentStyle}
|
||||
ref={this.contentDivRef}
|
||||
>
|
||||
{this.props.children}
|
||||
{children}
|
||||
</div>
|
||||
</FocusTrap>
|
||||
{decoration === PopoverDecoration.Balloon && (
|
||||
<div
|
||||
className="popover-tip"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: TipSize * 2,
|
||||
height: TipSize * 2,
|
||||
...tipStyle,
|
||||
}}
|
||||
ref={this.tipDivRef}
|
||||
>
|
||||
<div
|
||||
className="popover-tip-border"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderWidth: `${TipSize}px`,
|
||||
borderRightWidth: `${TipSize - 1}px`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="popover-tip-background"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderWidth: `${TipSize}px`,
|
||||
borderRightWidth: `${TipSize - 1}px`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return trapFocus !== false ? (
|
||||
<FocusTrap focusTrapOptions={this.focusTrapOptions}>{content}</FocusTrap>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
private getClassNameForCaret() {
|
||||
return `popover-caret-${
|
||||
this.props.caretPosition ?? PopoverCaretPosition.None
|
||||
}`
|
||||
private getFloatingPlacementForAnchorPosition(): Placement {
|
||||
const { anchorPosition } = this.props
|
||||
switch (anchorPosition) {
|
||||
case PopoverAnchorPosition.Top:
|
||||
return 'top'
|
||||
case PopoverAnchorPosition.TopLeft:
|
||||
return 'top-start'
|
||||
case PopoverAnchorPosition.TopRight:
|
||||
return 'top-end'
|
||||
case PopoverAnchorPosition.Left:
|
||||
return 'left'
|
||||
case PopoverAnchorPosition.LeftTop:
|
||||
return 'left-start'
|
||||
case PopoverAnchorPosition.LeftBottom:
|
||||
return 'left-end'
|
||||
case PopoverAnchorPosition.Right:
|
||||
return 'right'
|
||||
case PopoverAnchorPosition.RightTop:
|
||||
return 'right-start'
|
||||
case PopoverAnchorPosition.RightBottom:
|
||||
return 'right-end'
|
||||
case PopoverAnchorPosition.Bottom:
|
||||
return 'bottom'
|
||||
case PopoverAnchorPosition.BottomLeft:
|
||||
return 'bottom-start'
|
||||
case PopoverAnchorPosition.BottomRight:
|
||||
return 'bottom-end'
|
||||
default:
|
||||
assertNever(anchorPosition, 'Unknown anchor position')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,9 @@ interface IRadioButtonProps<T> {
|
|||
|
||||
/** Optional: The tab index of the radio button */
|
||||
readonly tabIndex?: number
|
||||
|
||||
/** Whether the textarea field should auto focus when mounted. */
|
||||
readonly autoFocus?: boolean
|
||||
}
|
||||
|
||||
interface IRadioButtonState {
|
||||
|
@ -62,6 +65,7 @@ export class RadioButton<T extends string> extends React.Component<
|
|||
checked={this.props.checked}
|
||||
onChange={this.onSelected}
|
||||
tabIndex={this.props.tabIndex}
|
||||
autoFocus={this.props.autoFocus}
|
||||
/>
|
||||
<label htmlFor={this.state.inputId}>
|
||||
{this.props.label ?? this.props.children}
|
||||
|
|
|
@ -74,7 +74,6 @@ export class TextArea extends React.Component<ITextAreaProps, {}> {
|
|||
{this.props.label}
|
||||
|
||||
<textarea
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={this.props.autoFocus}
|
||||
className={this.props.textareaClassName}
|
||||
disabled={this.props.disabled}
|
||||
|
|
|
@ -25,6 +25,9 @@ export interface ITextBoxProps {
|
|||
/** Whether the input field is disabled. */
|
||||
readonly disabled?: boolean
|
||||
|
||||
/** Whether the input field is read-only. */
|
||||
readonly readOnly?: boolean
|
||||
|
||||
/** Indicates if input field should be required */
|
||||
readonly required?: boolean
|
||||
|
||||
|
@ -256,16 +259,15 @@ export class TextBox extends React.Component<ITextBoxProps, ITextBoxState> {
|
|||
})}
|
||||
>
|
||||
{label && <label htmlFor={inputId}>{label}</label>}
|
||||
|
||||
<input
|
||||
id={inputId}
|
||||
ref={this.onInputRef}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={this.props.autoFocus}
|
||||
disabled={this.props.disabled}
|
||||
type={this.props.type}
|
||||
readOnly={this.props.readOnly}
|
||||
type={this.props.type ?? 'text'}
|
||||
placeholder={this.props.placeholder}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
|
|
|
@ -113,7 +113,7 @@ export class ToggledtippedContent extends React.Component<
|
|||
{children}
|
||||
{this.state.tooltipVisible && (
|
||||
<AriaLiveContainer
|
||||
shouldForceChange={this.shouldForceAriaLiveMessage}
|
||||
trackedUserInput={this.shouldForceAriaLiveMessage}
|
||||
>
|
||||
{tooltip}
|
||||
</AriaLiveContainer>
|
||||
|
|
|
@ -78,7 +78,6 @@ export class TwoFactorAuthentication extends React.Component<
|
|||
<TextBox
|
||||
label="Authentication code"
|
||||
disabled={textEntryDisabled}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true}
|
||||
onValueChanged={this.onOTPChange}
|
||||
/>
|
||||
|
|
|
@ -129,26 +129,28 @@ export class NoRepositoriesView extends React.Component<
|
|||
public render() {
|
||||
return (
|
||||
<UiView id="no-repositories">
|
||||
<header>
|
||||
<h1>Let's get started!</h1>
|
||||
<p>Add a repository to GitHub Desktop to start collaborating</p>
|
||||
</header>
|
||||
<section aria-label="Let's get started!">
|
||||
<header>
|
||||
<h1>Let's get started!</h1>
|
||||
<p>Add a repository to GitHub Desktop to start collaborating</p>
|
||||
</header>
|
||||
|
||||
<div className="content">
|
||||
{this.renderGetStartedActions()}
|
||||
{this.renderRepositoryList()}
|
||||
</div>
|
||||
<div className="content">
|
||||
{this.renderGetStartedActions()}
|
||||
{this.renderRepositoryList()}
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="no-repositories-graphic-top"
|
||||
src={WelcomeLeftTopImageUri}
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
className="no-repositories-graphic-bottom"
|
||||
src={WelcomeLeftBottomImageUri}
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
className="no-repositories-graphic-top"
|
||||
src={WelcomeLeftTopImageUri}
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
className="no-repositories-graphic-bottom"
|
||||
src={WelcomeLeftBottomImageUri}
|
||||
alt=""
|
||||
/>
|
||||
</section>
|
||||
</UiView>
|
||||
)
|
||||
}
|
||||
|
@ -180,6 +182,12 @@ export class NoRepositoriesView extends React.Component<
|
|||
}
|
||||
}
|
||||
|
||||
private isUserSignedIn() {
|
||||
return (
|
||||
this.props.dotComAccount !== null || this.props.enterpriseAccount !== null
|
||||
)
|
||||
}
|
||||
|
||||
private getSelectedAccount() {
|
||||
const { selectedTab } = this.state
|
||||
if (selectedTab === AccountTab.dotCom) {
|
||||
|
@ -346,24 +354,22 @@ export class NoRepositoriesView extends React.Component<
|
|||
symbol: OcticonSymbolType,
|
||||
title: string,
|
||||
onClick: () => void,
|
||||
type?: 'submit'
|
||||
type?: 'submit',
|
||||
autoFocus?: boolean
|
||||
) {
|
||||
return (
|
||||
<li>
|
||||
<Button onClick={onClick} type={type}>
|
||||
<span>
|
||||
<Button onClick={onClick} type={type} autoFocus={autoFocus}>
|
||||
<Octicon symbol={symbol} />
|
||||
<div>{title}</div>
|
||||
</Button>
|
||||
</li>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private renderTutorialRepositoryButton() {
|
||||
// No tutorial if you're not signed in.
|
||||
if (
|
||||
this.props.dotComAccount === null &&
|
||||
this.props.enterpriseAccount === null
|
||||
) {
|
||||
if (!this.isUserSignedIn()) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -394,7 +400,9 @@ export class NoRepositoriesView extends React.Component<
|
|||
__DARWIN__
|
||||
? 'Clone a Repository from the Internet…'
|
||||
: 'Clone a repository from the Internet…',
|
||||
this.onShowClone
|
||||
this.onShowClone,
|
||||
undefined,
|
||||
!this.isUserSignedIn()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -421,12 +429,12 @@ export class NoRepositoriesView extends React.Component<
|
|||
private renderGetStartedActions() {
|
||||
return (
|
||||
<div className="content-pane">
|
||||
<ul className="button-group">
|
||||
<div className="button-group">
|
||||
{this.renderTutorialRepositoryButton()}
|
||||
{this.renderCloneButton()}
|
||||
{this.renderCreateRepositoryButton()}
|
||||
{this.renderAddExistingRepositoryButton()}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="drag-drop-info">
|
||||
<Octicon symbol={OcticonSymbol.lightBulb} />
|
||||
|
|
|
@ -61,6 +61,13 @@ interface IOpenPullRequestDialogProps {
|
|||
/** Label for selected external editor */
|
||||
readonly externalEditorLabel?: string
|
||||
|
||||
/**
|
||||
* Callback to open a selected file using the configured external editor
|
||||
*
|
||||
* @param fullPath The full path to the file on disk
|
||||
*/
|
||||
readonly onOpenInExternalEditor: (fullPath: string) => void
|
||||
|
||||
/** Width to use for the files list pane in the files changed view */
|
||||
readonly fileListWidth: IConstrainedValue
|
||||
|
||||
|
@ -168,6 +175,7 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
|||
selectedFile={file}
|
||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||
repository={repository}
|
||||
onOpenInExternalEditor={this.props.onOpenInExternalEditor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,13 @@ interface IPullRequestFilesChangedProps {
|
|||
/** If the latest commit of the pull request is not local, this will contain
|
||||
* it's SHA */
|
||||
readonly nonLocalCommitSHA: string | null
|
||||
|
||||
/**
|
||||
* Callback to open a selected file using the configured external editor
|
||||
*
|
||||
* @param fullPath The full path to the file on disk
|
||||
*/
|
||||
readonly onOpenInExternalEditor: (fullPath: string) => void
|
||||
}
|
||||
|
||||
interface IPullRequestFilesChangedState {
|
||||
|
@ -224,6 +231,13 @@ export class PullRequestFilesChanged extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private onRowDoubleClick = (row: number) => {
|
||||
const files = this.props.files
|
||||
const file = files[row]
|
||||
|
||||
this.props.onOpenInExternalEditor(file.path)
|
||||
}
|
||||
|
||||
private renderHeader() {
|
||||
const { hideWhitespaceInDiff } = this.props
|
||||
const { showSideBySideDiff } = this.state
|
||||
|
@ -261,6 +275,7 @@ export class PullRequestFilesChanged extends React.Component<
|
|||
selectedFile={selectedFile}
|
||||
availableWidth={clamp(fileListWidth)}
|
||||
onContextMenu={this.onFileContextMenu}
|
||||
onRowDoubleClick={this.onRowDoubleClick}
|
||||
/>
|
||||
</Resizable>
|
||||
)
|
||||
|
|
|
@ -11,6 +11,7 @@ interface IGitProps {
|
|||
readonly name: string
|
||||
readonly email: string
|
||||
readonly defaultBranch: string
|
||||
readonly isLoadingGitConfig: boolean
|
||||
|
||||
readonly dotComAccount: Account | null
|
||||
readonly enterpriseAccount: Account | null
|
||||
|
@ -40,17 +41,29 @@ export class Git extends React.Component<IGitProps, IGitState> {
|
|||
super(props)
|
||||
|
||||
this.state = {
|
||||
defaultBranchIsOther: !SuggestedBranchNames.includes(
|
||||
this.props.defaultBranch
|
||||
),
|
||||
defaultBranchIsOther: this.isDefaultBranchOther(),
|
||||
}
|
||||
}
|
||||
|
||||
private isDefaultBranchOther = () => {
|
||||
return (
|
||||
!this.props.isLoadingGitConfig &&
|
||||
!SuggestedBranchNames.includes(this.props.defaultBranch)
|
||||
)
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IGitProps) {
|
||||
if (this.props.defaultBranch === prevProps.defaultBranch) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
defaultBranchIsOther: this.isDefaultBranchOther(),
|
||||
})
|
||||
|
||||
// Focus the text input that allows the user to enter a custom
|
||||
// branch name when the user has selected "Other...".
|
||||
if (
|
||||
this.props.defaultBranch !== prevProps.defaultBranch &&
|
||||
this.props.defaultBranch === OtherNameForDefaultBranch &&
|
||||
this.defaultBranchInputRef.current !== null
|
||||
) {
|
||||
|
@ -72,6 +85,7 @@ export class Git extends React.Component<IGitProps, IGitState> {
|
|||
<GitConfigUserForm
|
||||
email={this.props.email}
|
||||
name={this.props.name}
|
||||
isLoadingGitConfig={this.props.isLoadingGitConfig}
|
||||
enterpriseAccount={this.props.enterpriseAccount}
|
||||
dotComAccount={this.props.dotComAccount}
|
||||
onEmailChanged={this.props.onEmailChanged}
|
||||
|
@ -106,11 +120,13 @@ export class Git extends React.Component<IGitProps, IGitState> {
|
|||
<div className="default-branch-component">
|
||||
<h2>Default branch name for new repositories</h2>
|
||||
|
||||
{SuggestedBranchNames.map((branchName: string) => (
|
||||
{SuggestedBranchNames.map((branchName: string, i: number) => (
|
||||
<RadioButton
|
||||
key={branchName}
|
||||
checked={
|
||||
!defaultBranchIsOther && this.props.defaultBranch === branchName
|
||||
(!defaultBranchIsOther &&
|
||||
this.props.defaultBranch === branchName) ||
|
||||
(this.props.isLoadingGitConfig && i === 0)
|
||||
}
|
||||
value={branchName}
|
||||
label={branchName}
|
||||
|
|
|
@ -99,6 +99,8 @@ interface IPreferencesState {
|
|||
readonly repositoryIndicatorsEnabled: boolean
|
||||
|
||||
readonly initiallySelectedTheme: ApplicationTheme
|
||||
|
||||
readonly isLoadingGitConfig: boolean
|
||||
}
|
||||
|
||||
/** The app-level preferences component. */
|
||||
|
@ -134,6 +136,7 @@ export class Preferences extends React.Component<
|
|||
selectedShell: this.props.selectedShell,
|
||||
repositoryIndicatorsEnabled: this.props.repositoryIndicatorsEnabled,
|
||||
initiallySelectedTheme: this.props.selectedTheme,
|
||||
isLoadingGitConfig: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,6 +193,7 @@ export class Preferences extends React.Component<
|
|||
uncommittedChangesStrategy: this.props.uncommittedChangesStrategy,
|
||||
availableShells,
|
||||
availableEditors,
|
||||
isLoadingGitConfig: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -329,6 +333,7 @@ export class Preferences extends React.Component<
|
|||
onNameChanged={this.onCommitterNameChanged}
|
||||
onEmailChanged={this.onCommitterEmailChanged}
|
||||
onDefaultBranchChanged={this.onDefaultBranchChanged}
|
||||
isLoadingGitConfig={this.state.isLoadingGitConfig}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -23,6 +23,7 @@ import { encodePathAsUrl } from '../../lib/path'
|
|||
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||
import memoizeOne from 'memoize-one'
|
||||
import { KeyboardShortcut } from '../keyboard-shortcut/keyboard-shortcut'
|
||||
import { generateRepositoryListContextMenu } from '../repositories-list/repository-list-item-context-menu'
|
||||
|
||||
const BlankSlateImage = encodePathAsUrl(__dirname, 'static/empty-no-repo.svg')
|
||||
|
||||
|
@ -75,6 +76,10 @@ interface IRepositoriesListProps {
|
|||
readonly dispatcher: Dispatcher
|
||||
}
|
||||
|
||||
interface IRepositoriesListState {
|
||||
readonly newRepositoryMenuExpanded: boolean
|
||||
}
|
||||
|
||||
const RowHeight = 29
|
||||
|
||||
/**
|
||||
|
@ -101,7 +106,7 @@ function findMatchingListItem(
|
|||
/** The list of user-added repositories. */
|
||||
export class RepositoriesList extends React.Component<
|
||||
IRepositoriesListProps,
|
||||
{}
|
||||
IRepositoriesListState
|
||||
> {
|
||||
/**
|
||||
* A memoized function for grouping repositories for display
|
||||
|
@ -130,6 +135,14 @@ export class RepositoriesList extends React.Component<
|
|||
*/
|
||||
private getSelectedListItem = memoizeOne(findMatchingListItem)
|
||||
|
||||
public constructor(props: IRepositoriesListProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
newRepositoryMenuExpanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
private renderItem = (item: IRepositoryListItem, matches: IMatches) => {
|
||||
const repository = item.repository
|
||||
return (
|
||||
|
@ -137,18 +150,6 @@ export class RepositoriesList extends React.Component<
|
|||
key={repository.id}
|
||||
repository={repository}
|
||||
needsDisambiguation={item.needsDisambiguation}
|
||||
askForConfirmationOnRemoveRepository={
|
||||
this.props.askForConfirmationOnRemoveRepository
|
||||
}
|
||||
onRemoveRepository={this.props.onRemoveRepository}
|
||||
onShowRepository={this.props.onShowRepository}
|
||||
onViewOnGitHub={this.props.onViewOnGitHub}
|
||||
onOpenInShell={this.props.onOpenInShell}
|
||||
onOpenInExternalEditor={this.props.onOpenInExternalEditor}
|
||||
onChangeRepositoryAlias={this.onChangeRepositoryAlias}
|
||||
onRemoveRepositoryAlias={this.onRemoveRepositoryAlias}
|
||||
externalEditorLabel={this.props.externalEditorLabel}
|
||||
shellLabel={this.props.shellLabel}
|
||||
matches={matches}
|
||||
aheadBehind={item.aheadBehind}
|
||||
changedFilesCount={item.changedFilesCount}
|
||||
|
@ -193,6 +194,30 @@ export class RepositoriesList extends React.Component<
|
|||
this.props.onSelectionChanged(item.repository)
|
||||
}
|
||||
|
||||
private onItemContextMenu = (
|
||||
item: IRepositoryListItem,
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
event.preventDefault()
|
||||
|
||||
const items = generateRepositoryListContextMenu({
|
||||
onRemoveRepository: this.props.onRemoveRepository,
|
||||
onShowRepository: this.props.onShowRepository,
|
||||
onOpenInShell: this.props.onOpenInShell,
|
||||
onOpenInExternalEditor: this.props.onOpenInExternalEditor,
|
||||
askForConfirmationOnRemoveRepository:
|
||||
this.props.askForConfirmationOnRemoveRepository,
|
||||
externalEditorLabel: this.props.externalEditorLabel,
|
||||
onChangeRepositoryAlias: this.onChangeRepositoryAlias,
|
||||
onRemoveRepositoryAlias: this.onRemoveRepositoryAlias,
|
||||
onViewOnGitHub: this.props.onViewOnGitHub,
|
||||
repository: item.repository,
|
||||
shellLabel: this.props.shellLabel,
|
||||
})
|
||||
|
||||
showContextualMenu(items)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const baseGroups = this.getRepositoryGroups(
|
||||
this.props.repositories,
|
||||
|
@ -233,6 +258,7 @@ export class RepositoriesList extends React.Component<
|
|||
repositories: this.props.repositories,
|
||||
filterText: this.props.filterText,
|
||||
}}
|
||||
onItemContextMenu={this.onItemContextMenu}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -243,6 +269,7 @@ export class RepositoriesList extends React.Component<
|
|||
<Button
|
||||
className="new-repository-button"
|
||||
onClick={this.onNewRepositoryButtonClick}
|
||||
ariaExpanded={this.state.newRepositoryMenuExpanded}
|
||||
>
|
||||
Add
|
||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
||||
|
@ -292,7 +319,10 @@ export class RepositoriesList extends React.Component<
|
|||
},
|
||||
]
|
||||
|
||||
showContextualMenu(items)
|
||||
this.setState({ newRepositoryMenuExpanded: true })
|
||||
showContextualMenu(items).then(() => {
|
||||
this.setState({ newRepositoryMenuExpanded: false })
|
||||
})
|
||||
}
|
||||
|
||||
private onCloneRepository = () => {
|
||||
|
|
|
@ -3,7 +3,6 @@ import * as React from 'react'
|
|||
import { Repository } from '../../models/repository'
|
||||
import { Octicon, iconForRepository } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { Repositoryish } from './group-repositories'
|
||||
import { HighlightText } from '../lib/highlight-text'
|
||||
import { IMatches } from '../../lib/fuzzy-find'
|
||||
|
@ -12,44 +11,13 @@ import classNames from 'classnames'
|
|||
import { createObservableRef } from '../lib/observable-ref'
|
||||
import { Tooltip } from '../lib/tooltip'
|
||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||
import { generateRepositoryListContextMenu } from './repository-list-item-context-menu'
|
||||
|
||||
interface IRepositoryListItemProps {
|
||||
readonly repository: Repositoryish
|
||||
|
||||
/** Whether the user has enabled the setting to confirm removing a repository from the app */
|
||||
readonly askForConfirmationOnRemoveRepository: boolean
|
||||
|
||||
/** Called when the repository should be removed. */
|
||||
readonly onRemoveRepository: (repository: Repositoryish) => void
|
||||
|
||||
/** Called when the repository should be shown in Finder/Explorer/File Manager. */
|
||||
readonly onShowRepository: (repository: Repositoryish) => void
|
||||
|
||||
/** Called when the repository should be opened on GitHub in the default web browser. */
|
||||
readonly onViewOnGitHub: (repository: Repositoryish) => void
|
||||
|
||||
/** Called when the repository should be shown in the shell. */
|
||||
readonly onOpenInShell: (repository: Repositoryish) => void
|
||||
|
||||
/** Called when the repository should be opened in an external editor */
|
||||
readonly onOpenInExternalEditor: (repository: Repositoryish) => void
|
||||
|
||||
/** Called when the repository alias should be changed */
|
||||
readonly onChangeRepositoryAlias: (repository: Repository) => void
|
||||
|
||||
/** Called when the repository alias should be removed */
|
||||
readonly onRemoveRepositoryAlias: (repository: Repository) => void
|
||||
|
||||
/** The current external editor selected by the user */
|
||||
readonly externalEditorLabel?: string
|
||||
|
||||
/** Does the repository need to be disambiguated in the list? */
|
||||
readonly needsDisambiguation: boolean
|
||||
|
||||
/** The label for the user's preferred shell. */
|
||||
readonly shellLabel: string
|
||||
|
||||
/** The characters in the repository name to highlight */
|
||||
readonly matches: IMatches
|
||||
|
||||
|
@ -86,11 +54,7 @@ export class RepositoryListItem extends React.Component<
|
|||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
onContextMenu={this.onContextMenu}
|
||||
className="repository-list-item"
|
||||
ref={this.listItemRef}
|
||||
>
|
||||
<div className="repository-list-item" ref={this.listItemRef}>
|
||||
<Tooltip target={this.listItemRef}>{this.renderTooltip()}</Tooltip>
|
||||
|
||||
<Octicon
|
||||
|
@ -144,27 +108,6 @@ export class RepositoryListItem extends React.Component<
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private onContextMenu = (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault()
|
||||
|
||||
const items = generateRepositoryListContextMenu({
|
||||
onRemoveRepository: this.props.onRemoveRepository,
|
||||
onShowRepository: this.props.onShowRepository,
|
||||
onOpenInShell: this.props.onOpenInShell,
|
||||
onOpenInExternalEditor: this.props.onOpenInExternalEditor,
|
||||
askForConfirmationOnRemoveRepository:
|
||||
this.props.askForConfirmationOnRemoveRepository,
|
||||
externalEditorLabel: this.props.externalEditorLabel,
|
||||
onChangeRepositoryAlias: this.props.onChangeRepositoryAlias,
|
||||
onRemoveRepositoryAlias: this.props.onRemoveRepositoryAlias,
|
||||
onViewOnGitHub: this.props.onViewOnGitHub,
|
||||
repository: this.props.repository,
|
||||
shellLabel: this.props.shellLabel,
|
||||
})
|
||||
|
||||
showContextualMenu(items)
|
||||
}
|
||||
}
|
||||
|
||||
const renderRepoIndicators: React.FunctionComponent<{
|
||||
|
|
|
@ -14,6 +14,7 @@ interface IGitConfigProps {
|
|||
readonly email: string
|
||||
readonly globalName: string
|
||||
readonly globalEmail: string
|
||||
readonly isLoadingGitConfig: boolean
|
||||
|
||||
readonly onGitConfigLocationChanged: (value: GitConfigLocation) => void
|
||||
readonly onNameChanged: (name: string) => void
|
||||
|
@ -78,6 +79,7 @@ export class GitConfig extends React.Component<IGitConfigProps> {
|
|||
disabled={this.props.gitConfigLocation === GitConfigLocation.Global}
|
||||
onEmailChanged={this.props.onEmailChanged}
|
||||
onNameChanged={this.props.onNameChanged}
|
||||
isLoadingGitConfig={this.props.isLoadingGitConfig}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
|
@ -28,7 +28,7 @@ export class GitIgnore extends React.Component<IGitIgnoreProps, {}> {
|
|||
placeholder="Ignored files"
|
||||
value={this.props.text || ''}
|
||||
onValueChanged={this.props.onIgnoreTextChanged}
|
||||
rows={6}
|
||||
textareaClassName="gitignore"
|
||||
/>
|
||||
</DialogContent>
|
||||
)
|
||||
|
|
|
@ -65,6 +65,7 @@ interface IRepositorySettingsState {
|
|||
readonly initialCommitterEmail: string | null
|
||||
readonly errors?: ReadonlyArray<JSX.Element | string>
|
||||
readonly forkContributionTarget: ForkContributionTarget
|
||||
readonly isLoadingGitConfig: boolean
|
||||
}
|
||||
|
||||
export class RepositorySettings extends React.Component<
|
||||
|
@ -91,6 +92,7 @@ export class RepositorySettings extends React.Component<
|
|||
initialGitConfigLocation: GitConfigLocation.Global,
|
||||
initialCommitterName: null,
|
||||
initialCommitterEmail: null,
|
||||
isLoadingGitConfig: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,6 +145,7 @@ export class RepositorySettings extends React.Component<
|
|||
initialGitConfigLocation: gitConfigLocation,
|
||||
initialCommitterName: localCommitterName,
|
||||
initialCommitterEmail: localCommitterEmail,
|
||||
isLoadingGitConfig: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -202,28 +205,16 @@ export class RepositorySettings extends React.Component<
|
|||
|
||||
<div className="active-tab">{this.renderActiveTab()}</div>
|
||||
</div>
|
||||
{this.renderFooter()}
|
||||
<DialogFooter>
|
||||
<OkCancelButtonGroup
|
||||
okButtonText="Save"
|
||||
okButtonDisabled={this.state.saveDisabled}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
private renderFooter() {
|
||||
const tab = this.state.selectedTab
|
||||
const remote = this.state.remote
|
||||
if (tab === RepositorySettingsTab.Remote && !remote) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogFooter>
|
||||
<OkCancelButtonGroup
|
||||
okButtonText="Save"
|
||||
okButtonDisabled={this.state.saveDisabled}
|
||||
/>
|
||||
</DialogFooter>
|
||||
)
|
||||
}
|
||||
|
||||
private renderActiveTab() {
|
||||
const tab = this.state.selectedTab
|
||||
switch (tab) {
|
||||
|
@ -277,6 +268,7 @@ export class RepositorySettings extends React.Component<
|
|||
globalEmail={this.state.globalCommitterEmail}
|
||||
onNameChanged={this.onCommitterNameChanged}
|
||||
onEmailChanged={this.onCommitterEmailChanged}
|
||||
isLoadingGitConfig={this.state.isLoadingGitConfig}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -380,6 +380,7 @@ export class RepositoryView extends React.Component<
|
|||
onOpenSubmodule={this.onOpenSubmodule}
|
||||
onChangeImageDiffType={this.onChangeImageDiffType}
|
||||
onHideWhitespaceInDiffChanged={this.onHideWhitespaceInDiffChanged}
|
||||
onOpenInExternalEditor={this.props.onOpenInExternalEditor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -519,6 +520,7 @@ export class RepositoryView extends React.Component<
|
|||
this.props.askForConfirmationOnDiscardChanges
|
||||
}
|
||||
onDiffOptionsOpened={this.onDiffOptionsOpened}
|
||||
onOpenInExternalEditor={this.props.onOpenInExternalEditor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
import * as React from 'react'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import {
|
||||
|
@ -21,6 +20,7 @@ import { getDotComAPIEndpoint } from '../../lib/api'
|
|||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { Button } from '../lib/button'
|
||||
import { HorizontalRule } from '../lib/horizontal-rule'
|
||||
import { PasswordTextBox } from '../lib/password-text-box'
|
||||
|
||||
interface ISignInProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
|
@ -250,10 +250,9 @@ export class SignIn extends React.Component<ISignInProps, ISignInState> {
|
|||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<TextBox
|
||||
<PasswordTextBox
|
||||
label="Password"
|
||||
value={this.state.password}
|
||||
type="password"
|
||||
onValueChanged={this.onPasswordChanged}
|
||||
/>
|
||||
</Row>
|
||||
|
|
|
@ -2,8 +2,8 @@ import * as React from 'react'
|
|||
import { Dialog, DialogContent, DialogFooter } from '../dialog'
|
||||
import { Row } from '../lib/row'
|
||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { TextBox } from '../lib/text-box'
|
||||
import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
||||
import { PasswordTextBox } from '../lib/password-text-box'
|
||||
|
||||
interface ISSHKeyPassphraseProps {
|
||||
readonly keyPath: string
|
||||
|
@ -43,10 +43,9 @@ export class SSHKeyPassphrase extends React.Component<
|
|||
>
|
||||
<DialogContent>
|
||||
<Row>
|
||||
<TextBox
|
||||
<PasswordTextBox
|
||||
label={`Enter passphrase for key '${this.props.keyPath}':`}
|
||||
value={this.state.passphrase}
|
||||
type="password"
|
||||
onValueChanged={this.onValueChanged}
|
||||
/>
|
||||
</Row>
|
||||
|
|
|
@ -2,8 +2,8 @@ import * as React from 'react'
|
|||
import { Dialog, DialogContent, DialogFooter } from '../dialog'
|
||||
import { Row } from '../lib/row'
|
||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { TextBox } from '../lib/text-box'
|
||||
import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
||||
import { PasswordTextBox } from '../lib/password-text-box'
|
||||
|
||||
interface ISSHUserPasswordProps {
|
||||
readonly username: string
|
||||
|
@ -43,10 +43,9 @@ export class SSHUserPassword extends React.Component<
|
|||
>
|
||||
<DialogContent>
|
||||
<Row>
|
||||
<TextBox
|
||||
<PasswordTextBox
|
||||
label={`Enter password for '${this.props.username}':`}
|
||||
value={this.state.password}
|
||||
type="password"
|
||||
onValueChanged={this.onValueChanged}
|
||||
/>
|
||||
</Row>
|
||||
|
|
|
@ -53,6 +53,13 @@ interface IStashDiffViewerProps {
|
|||
|
||||
/** Called when the user requests to open a submodule. */
|
||||
readonly onOpenSubmodule: (fullPath: string) => void
|
||||
|
||||
/**
|
||||
* Called to open a file using the user's configured applications
|
||||
*
|
||||
* @param path The path of the file relative to the root of the repository
|
||||
*/
|
||||
readonly onOpenInExternalEditor: (path: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,11 +71,23 @@ export class StashDiffViewer extends React.PureComponent<IStashDiffViewerProps>
|
|||
private onSelectedFileChanged = (file: CommittedFileChange) =>
|
||||
this.props.dispatcher.selectStashedFile(this.props.repository, file)
|
||||
|
||||
private onRowDoubleClick = (row: number) => {
|
||||
const files = this.getFiles()
|
||||
const file = files[row]
|
||||
|
||||
this.props.onOpenInExternalEditor(file.path)
|
||||
}
|
||||
|
||||
private onResize = (width: number) =>
|
||||
this.props.dispatcher.setStashedFilesWidth(width)
|
||||
|
||||
private onReset = () => this.props.dispatcher.resetStashedFilesWidth()
|
||||
|
||||
private getFiles = () =>
|
||||
this.props.stashEntry.files.kind === StashedChangesLoadStates.Loaded
|
||||
? this.props.stashEntry.files.files
|
||||
: new Array<CommittedFileChange>()
|
||||
|
||||
public render() {
|
||||
const {
|
||||
stashEntry,
|
||||
|
@ -83,10 +102,7 @@ export class StashDiffViewer extends React.PureComponent<IStashDiffViewerProps>
|
|||
onChangeImageDiffType,
|
||||
onOpenSubmodule,
|
||||
} = this.props
|
||||
const files =
|
||||
stashEntry.files.kind === StashedChangesLoadStates.Loaded
|
||||
? stashEntry.files.files
|
||||
: new Array<CommittedFileChange>()
|
||||
const files = this.getFiles()
|
||||
|
||||
const diffComponent =
|
||||
selectedStashedFile !== null ? (
|
||||
|
@ -133,6 +149,7 @@ export class StashDiffViewer extends React.PureComponent<IStashDiffViewerProps>
|
|||
onSelectedFileChanged={this.onSelectedFileChanged}
|
||||
selectedFile={selectedStashedFile}
|
||||
availableWidth={availableWidth}
|
||||
onRowDoubleClick={this.onRowDoubleClick}
|
||||
/>
|
||||
</Resizable>
|
||||
{diffComponent}
|
||||
|
|
|
@ -64,23 +64,12 @@ interface IBranchDropdownProps {
|
|||
/** Map from the emoji shortcut (e.g., :+1:) to the image's local path. */
|
||||
readonly emoji: Map<string, string>
|
||||
}
|
||||
interface IBranchDropdownState {
|
||||
readonly badgeBottom: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A drop down for selecting the currently checked out branch.
|
||||
*/
|
||||
export class BranchDropdown extends React.Component<
|
||||
IBranchDropdownProps,
|
||||
IBranchDropdownState
|
||||
> {
|
||||
public constructor(props: IBranchDropdownProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
badgeBottom: 0,
|
||||
}
|
||||
}
|
||||
export class BranchDropdown extends React.Component<IBranchDropdownProps> {
|
||||
private badgeRef: HTMLElement | null = null
|
||||
|
||||
private renderBranchFoldout = (): JSX.Element | null => {
|
||||
const repositoryState = this.props.repositoryState
|
||||
|
@ -306,10 +295,6 @@ export class BranchDropdown extends React.Component<
|
|||
this.openPopover()
|
||||
}
|
||||
|
||||
private updateBadgeBottomPosition = (badgeBottom: number) => {
|
||||
this.setState({ badgeBottom })
|
||||
}
|
||||
|
||||
private openPopover = () => {
|
||||
this.props.dispatcher.setShowCIStatusPopover(true)
|
||||
}
|
||||
|
@ -361,12 +346,16 @@ export class BranchDropdown extends React.Component<
|
|||
dispatcher={this.props.dispatcher}
|
||||
repository={pr.base.gitHubRepository}
|
||||
branchName={currentBranchName}
|
||||
badgeBottom={this.state.badgeBottom}
|
||||
anchor={this.badgeRef}
|
||||
closePopover={this.closePopover}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private onBadgeRef = (ref: HTMLSpanElement | null) => {
|
||||
this.badgeRef = ref
|
||||
}
|
||||
|
||||
private renderPullRequestInfo() {
|
||||
const pr = this.props.currentPullRequest
|
||||
|
||||
|
@ -379,8 +368,8 @@ export class BranchDropdown extends React.Component<
|
|||
number={pr.pullRequestNumber}
|
||||
dispatcher={this.props.dispatcher}
|
||||
repository={pr.base.gitHubRepository}
|
||||
onBadgeRef={this.onBadgeRef}
|
||||
onBadgeClick={this.onBadgeClick}
|
||||
onBadgeBottomPositionUpdate={this.updateBadgeBottomPosition}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -111,6 +111,13 @@ export interface IToolbarButtonProps {
|
|||
readonly ariaExpanded?: boolean
|
||||
readonly ariaHaspopup?: AriaHasPopupType
|
||||
|
||||
/**
|
||||
* Typically the contents of a button serve the purpose of describing the
|
||||
* buttons use. However, ariaLabel can be used if the contents do not suffice.
|
||||
* Such as when a button wraps an image and there is no text.
|
||||
*/
|
||||
readonly ariaLabel?: string
|
||||
|
||||
/**
|
||||
* Whether to only show the tooltip when the tooltip target overflows its
|
||||
* bounds. Typically this is used in conjunction with an ellipsis CSS ruleset.
|
||||
|
@ -224,6 +231,7 @@ export class ToolbarButton extends React.Component<IToolbarButtonProps, {}> {
|
|||
role={this.props.role}
|
||||
ariaExpanded={this.props.ariaExpanded}
|
||||
ariaHaspopup={this.props.ariaHaspopup}
|
||||
ariaLabel={this.props.ariaLabel}
|
||||
>
|
||||
{progress}
|
||||
{icon}
|
||||
|
|
|
@ -195,6 +195,13 @@ export interface IToolbarDropdownProps {
|
|||
* the tooltip.
|
||||
*/
|
||||
readonly isOverflowed?: ((target: TooltipTarget) => boolean) | boolean
|
||||
|
||||
/**
|
||||
* Typically the contents of a button serve the purpose of describing the
|
||||
* buttons use. However, ariaLabel can be used if the contents do not suffice.
|
||||
* Such as when a button wraps an image and there is no text.
|
||||
*/
|
||||
readonly ariaLabel?: string
|
||||
}
|
||||
|
||||
interface IToolbarDropdownState {
|
||||
|
@ -256,6 +263,9 @@ export class ToolbarDropdown extends React.Component<
|
|||
<ToolbarButton
|
||||
className="toolbar-dropdown-arrow-button"
|
||||
onClick={this.onToggleDropdownClick}
|
||||
ariaExpanded={this.isOpen}
|
||||
ariaHaspopup={true}
|
||||
ariaLabel={this.props.ariaLabel}
|
||||
>
|
||||
{dropdownIcon}
|
||||
</ToolbarButton>
|
||||
|
@ -418,14 +428,11 @@ export class ToolbarDropdown extends React.Component<
|
|||
this.props.className
|
||||
)
|
||||
|
||||
const ariaExpanded = this.props.dropdownState === 'open' ? 'true' : 'false'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onKeyDown={this.props.onKeyDown}
|
||||
role={this.props.role}
|
||||
aria-expanded={ariaExpanded}
|
||||
onDragOver={this.props.onDragOver}
|
||||
ref={this.rootDiv}
|
||||
>
|
||||
|
@ -450,6 +457,11 @@ export class ToolbarDropdown extends React.Component<
|
|||
this.props.onlyShowTooltipWhenOverflowed
|
||||
}
|
||||
isOverflowed={this.props.isOverflowed}
|
||||
ariaExpanded={
|
||||
this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption
|
||||
? undefined
|
||||
: this.isOpen
|
||||
}
|
||||
ariaHaspopup={this.props.buttonAriaHaspopup}
|
||||
>
|
||||
{this.props.children}
|
||||
|
|
|
@ -179,6 +179,7 @@ export class PushPullButton extends React.Component<IPushPullButtonProps> {
|
|||
buttonClassName: 'push-pull-button',
|
||||
style: ToolbarButtonStyle.Subtitle,
|
||||
dropdownStyle: ToolbarDropdownStyle.MultiOption,
|
||||
ariaLabel: 'Push, pull, fetch options',
|
||||
dropdownState: this.props.isDropdownOpen ? 'open' : 'closed',
|
||||
onDropdownStateChanged: this.props.onDropdownStateChanged,
|
||||
}
|
||||
|
|
|
@ -269,7 +269,7 @@ export class TutorialPanel extends React.Component<
|
|||
private onPreferencesClick = () => {
|
||||
this.props.dispatcher.showPopup({
|
||||
type: PopupType.Preferences,
|
||||
initialSelectedTab: PreferencesTab.Advanced,
|
||||
initialSelectedTab: PreferencesTab.Integrations,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ interface IConfigureGitProps {
|
|||
export class ConfigureGit extends React.Component<IConfigureGitProps, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<div id="configure-git">
|
||||
<section id="configure-git" aria-label="Configure Git">
|
||||
<h1 className="welcome-title">Configure Git</h1>
|
||||
<p className="welcome-text">
|
||||
This is used to identify the commits you create. Anyone will be able
|
||||
|
@ -28,7 +28,7 @@ export class ConfigureGit extends React.Component<IConfigureGitProps, {}> {
|
|||
>
|
||||
<Button onClick={this.cancel}>Cancel</Button>
|
||||
</ConfigureGitUser>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -24,13 +24,16 @@ export class SignInEnterprise extends React.Component<
|
|||
}
|
||||
|
||||
return (
|
||||
<div id="sign-in-enterprise">
|
||||
<section
|
||||
id="sign-in-enterprise"
|
||||
aria-label="Sign in to your GitHub Enterprise"
|
||||
>
|
||||
<h1 className="welcome-title">Sign in to your GitHub Enterprise</h1>
|
||||
|
||||
<SignIn signInState={state} dispatcher={this.props.dispatcher}>
|
||||
<Button onClick={this.cancel}>Cancel</Button>
|
||||
</SignIn>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -26,24 +26,19 @@ interface IStartProps {
|
|||
export class Start extends React.Component<IStartProps, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<div id="start">
|
||||
<section
|
||||
id="start"
|
||||
aria-label="Welcome to GitHub Desktop"
|
||||
aria-describedby="start-description"
|
||||
>
|
||||
<h1 className="welcome-title">Welcome to GitHub Desktop</h1>
|
||||
{!this.props.loadingBrowserAuth ? (
|
||||
<>
|
||||
<p className="welcome-text">
|
||||
<p id="start-description" className="welcome-text">
|
||||
GitHub Desktop is a seamless way to contribute to projects on
|
||||
GitHub and GitHub Enterprise. Sign in below to get started with
|
||||
your existing projects.
|
||||
</p>
|
||||
<p className="welcome-text">
|
||||
New to GitHub?{' '}
|
||||
<LinkButton
|
||||
uri={CreateAccountURL}
|
||||
className="create-account-link"
|
||||
>
|
||||
Create your free account.
|
||||
</LinkButton>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>{BrowserRedirectMessage}</p>
|
||||
|
@ -55,6 +50,7 @@ export class Start extends React.Component<IStartProps, {}> {
|
|||
className="button-with-icon"
|
||||
disabled={this.props.loadingBrowserAuth}
|
||||
onClick={this.signInWithBrowser}
|
||||
autoFocus={true}
|
||||
>
|
||||
{this.props.loadingBrowserAuth && <Loading />}
|
||||
Sign in to GitHub.com
|
||||
|
@ -69,6 +65,12 @@ export class Start extends React.Component<IStartProps, {}> {
|
|||
)}
|
||||
</div>
|
||||
<div className="skip-action-container">
|
||||
<p className="welcome-text">
|
||||
New to GitHub?{' '}
|
||||
<LinkButton uri={CreateAccountURL} className="create-account-link">
|
||||
Create your free account.
|
||||
</LinkButton>
|
||||
</p>
|
||||
<LinkButton className="skip-button" onClick={this.skip}>
|
||||
Skip this step
|
||||
</LinkButton>
|
||||
|
@ -90,7 +92,7 @@ export class Start extends React.Component<IStartProps, {}> {
|
|||
Learn more about user metrics.
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GitHub Desktop</title>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
@import 'ui/configure-git-user';
|
||||
@import 'ui/form';
|
||||
@import 'ui/text-box';
|
||||
@import 'ui/password-text-box';
|
||||
@import 'ui/ref-name-text-box';
|
||||
@import 'ui/radio-button';
|
||||
@import 'ui/button';
|
||||
|
|
|
@ -12,8 +12,6 @@
|
|||
|
||||
.autocompletion-popup {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
z-index: var(--popup-z-index);
|
||||
width: 250px;
|
||||
|
||||
border-radius: var(--border-radius);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
.commit-message-avatar-component {
|
||||
// With this, the popover's absolute position will be relative to its parent
|
||||
position: relative;
|
||||
width: var(--text-field-height);
|
||||
height: var(--text-field-height);
|
||||
|
||||
.avatar-button {
|
||||
// override default button styles
|
||||
|
@ -16,7 +18,12 @@
|
|||
background-color: none;
|
||||
|
||||
border-radius: 50%;
|
||||
margin-right: var(--spacing-half);
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toggletip {
|
||||
|
@ -48,12 +55,6 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
section + section {
|
||||
margin-top: var(--spacing);
|
||||
}
|
||||
|
@ -65,6 +66,11 @@
|
|||
|
||||
.button-row {
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
min-width: 120px;
|
||||
margin-right: var(--spacing-half);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
|
@ -83,31 +89,7 @@
|
|||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
|
||||
@media (max-height: 500px) {
|
||||
bottom: 5px;
|
||||
top: unset !important;
|
||||
|
||||
&.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 138px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 330px) {
|
||||
&.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 118px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-button-component {
|
||||
|
|
|
@ -34,4 +34,9 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.git-config-loading {
|
||||
color: var(--text-secondary-color);
|
||||
height: 107px; // Height of name/email input to prevent jumping
|
||||
}
|
||||
}
|
||||
|
|
|
@ -407,7 +407,7 @@ dialog {
|
|||
width: 600px;
|
||||
|
||||
.dialog-content {
|
||||
min-height: 340px;
|
||||
min-height: 375px;
|
||||
}
|
||||
|
||||
// This is to ensure that the dialog content is accessible when zoomed in
|
||||
|
|
|
@ -66,11 +66,6 @@
|
|||
}
|
||||
|
||||
.popover-component {
|
||||
position: absolute;
|
||||
left: 28px;
|
||||
margin-left: -250px;
|
||||
top: 27px;
|
||||
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,6 @@
|
|||
}
|
||||
|
||||
.popover-component.whitespace-hint {
|
||||
position: absolute;
|
||||
width: 225px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
height: 0;
|
||||
|
||||
& > .content-pane {
|
||||
display: flex;
|
||||
|
@ -43,13 +44,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
ul.button-group {
|
||||
list-style-type: none;
|
||||
.button-group {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex-grow: 1;
|
||||
|
||||
li {
|
||||
span {
|
||||
margin-bottom: var(--spacing);
|
||||
width: 100%;
|
||||
|
||||
|
|
36
app/styles/ui/_password-text-box.scss
Normal file
36
app/styles/ui/_password-text-box.scss
Normal file
|
@ -0,0 +1,36 @@
|
|||
.password-text-box {
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
.text-box-component {
|
||||
input {
|
||||
padding-right: var(--spacing-triple);
|
||||
}
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 16px;
|
||||
margin-right: 0 !important;
|
||||
height: var(--text-field-height);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 15px !important;
|
||||
color: var(--text-secondary-color);
|
||||
|
||||
// Removing the default button styles
|
||||
overflow: inherit;
|
||||
text-overflow: inherit;
|
||||
white-space: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: none;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
}
|
|
@ -12,11 +12,18 @@
|
|||
}
|
||||
|
||||
.popover-dropdown-popover {
|
||||
position: absolute;
|
||||
min-height: 200px;
|
||||
width: 365px;
|
||||
padding: 0;
|
||||
margin-top: 25px;
|
||||
|
||||
.popover-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.popover-dropdown-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.popover-dropdown-header {
|
||||
padding: var(--spacing);
|
||||
|
@ -31,6 +38,8 @@
|
|||
|
||||
.popover-dropdown-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,31 +2,16 @@
|
|||
font-size: var(--font-size);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
|
||||
z-index: var(--foldout-z-index);
|
||||
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
border-radius: var(--border-radius);
|
||||
border: var(--base-border);
|
||||
|
||||
padding: var(--spacing-double);
|
||||
|
||||
box-shadow: var(--base-box-shadow);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
border: 8px solid transparent;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: 7px solid transparent;
|
||||
.popover-content {
|
||||
padding: var(--spacing-double);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
& > p:first-of-type,
|
||||
|
@ -87,130 +72,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Carets
|
||||
.popover-component.popover-caret-top-right {
|
||||
&::before,
|
||||
&::after {
|
||||
right: 20px;
|
||||
.popover-tip {
|
||||
* {
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -16px;
|
||||
margin-right: -9px;
|
||||
border-bottom-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: -14px;
|
||||
margin-right: -8px;
|
||||
border-bottom-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-top-left {
|
||||
&::before,
|
||||
&::after {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -16px;
|
||||
margin-right: -9px;
|
||||
border-bottom-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: -14px;
|
||||
margin-right: -8px;
|
||||
border-bottom-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-top {
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -16px;
|
||||
margin-right: -9px;
|
||||
border: 8px solid transparent;
|
||||
border-bottom-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: -14px;
|
||||
margin-right: -8px;
|
||||
border: 7px solid transparent;
|
||||
border-bottom-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-left-top {
|
||||
&::before,
|
||||
&::after {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -16px;
|
||||
margin-top: -9px;
|
||||
border-right-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -14px;
|
||||
margin-top: -8px;
|
||||
.popover-tip-background {
|
||||
border-right-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -16px;
|
||||
margin-bottom: -9px;
|
||||
.popover-tip-border {
|
||||
border-right-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -14px;
|
||||
margin-bottom: -8px;
|
||||
border-right-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-right-top {
|
||||
&::before,
|
||||
&::after {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: -16px;
|
||||
margin-top: -9px;
|
||||
border-left-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: -14px;
|
||||
margin-top: -8px;
|
||||
border-left-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-none {
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,6 @@
|
|||
|
||||
.row {
|
||||
.popover-component.whitespace-hint {
|
||||
position: fixed;
|
||||
width: 275px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@
|
|||
-webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/></svg>');
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
@include textboxish-disabled-styles;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.no-invalid-state) :not(:focus):invalid {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: var(--spacing-half);
|
||||
|
||||
.summary-field {
|
||||
flex: 1;
|
||||
|
@ -26,12 +27,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
width: var(--text-field-height);
|
||||
height: var(--text-field-height);
|
||||
}
|
||||
|
||||
&.with-length-hint input {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
.ci-check-list-popover {
|
||||
.popover-component {
|
||||
position: absolute;
|
||||
padding: 0px;
|
||||
width: 440px;
|
||||
top: 60;
|
||||
margin-left: -302px;
|
||||
|
||||
&::after {
|
||||
border-bottom-color: var(--box-alt-background-color);
|
||||
.popover-content {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.popover-tip-background {
|
||||
border-right-color: var(--box-alt-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ci-check-run-list-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.ci-check-run-list-header {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
#repository-settings {
|
||||
width: 600px;
|
||||
|
||||
.dialog-content {
|
||||
min-height: 305px;
|
||||
}
|
||||
|
||||
.no-remote {
|
||||
justify-content: space-between;
|
||||
|
||||
|
@ -37,4 +41,8 @@
|
|||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
textarea.gitignore {
|
||||
height: 130px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,27 +6,6 @@
|
|||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.popover-component {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 73px;
|
||||
width: 284px;
|
||||
|
||||
.call-to-action-bubble {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-xs);
|
||||
border: 1px solid var(--call-to-action-bubble-border-color);
|
||||
color: var(--call-to-action-bubble-color);
|
||||
padding: 1px 5px;
|
||||
border-radius: var(--border-radius);
|
||||
margin-left: var(--spacing-third);
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.draggable,
|
||||
.commit {
|
||||
width: 100%;
|
||||
|
|
|
@ -155,7 +155,7 @@ body > .tooltip,
|
|||
flex-direction: row;
|
||||
padding: var(--spacing-half) 0;
|
||||
|
||||
img {
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: var(--spacing);
|
||||
|
|
|
@ -96,4 +96,34 @@ describe('URL remote parsing', () => {
|
|||
expect(remote!.owner).toBe('hubot')
|
||||
expect(remote!.name).toBe('repo')
|
||||
})
|
||||
|
||||
it('does not parse invalid HTTP URLs when missing repo name', () => {
|
||||
const remote = parseRemote('https://github.com/someuser//')
|
||||
expect(remote).toBeNull()
|
||||
})
|
||||
|
||||
it('does not parse invalid SSH URLs when missing repo name ', () => {
|
||||
const remote = parseRemote('git@github.com:hubot/')
|
||||
expect(remote).toBeNull()
|
||||
})
|
||||
|
||||
it('does not parse invalid git URLs when missing repo name', () => {
|
||||
const remote = parseRemote('git:github.com/hubot/')
|
||||
expect(remote).toBeNull()
|
||||
})
|
||||
|
||||
it('does not parse invalid HTTP URLs when missing repo owner', () => {
|
||||
const remote = parseRemote('https://github.com//somerepo')
|
||||
expect(remote).toBeNull()
|
||||
})
|
||||
|
||||
it('does not parse invalid SSH URLs when missing repo owner', () => {
|
||||
const remote = parseRemote('git@github.com:/somerepo')
|
||||
expect(remote).toBeNull()
|
||||
})
|
||||
|
||||
it('does not parse invalid git URLs when missing repo owner', () => {
|
||||
const remote = parseRemote('git:github.com/hubot/')
|
||||
expect(remote).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -30,6 +30,25 @@
|
|||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@floating-ui/core@^1.2.6":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.2.6.tgz#d21ace437cc919cdd8f1640302fa8851e65e75c0"
|
||||
integrity sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==
|
||||
|
||||
"@floating-ui/dom@^1.2.7":
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.2.7.tgz#c123e4db014b07b97e996cd459245fa217049c6b"
|
||||
integrity sha512-DyqylONj1ZaBnzj+uBnVfzdjjCkFCL2aA9ESHLyUOGSqb03RpbLMImP1ekIQXYs4KLk9jAjJfZAU8hXfWSahEg==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.2.6"
|
||||
|
||||
"@floating-ui/react-dom@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.0.tgz#7514baac526c818892bbcc84e1c3115008c029f9"
|
||||
integrity sha512-Ke0oU3SeuABC2C4OFu2mSAwHIP5WUiV98O9YWoHV4Q5aT6E9k06DV0Khi5uYspR8xmmBk08t8ZDcz3TR3ARkEg==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.2.7"
|
||||
|
||||
"@github/alive-client@^0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@github/alive-client/-/alive-client-0.0.2.tgz#77ccf103483f87930ca25ee8d69c4ab7b86be53f"
|
||||
|
|
|
@ -7,6 +7,28 @@
|
|||
"[Fixed] Multi-commit diffing produces the same results whether you select up to down or down to up - #15883",
|
||||
"[Removed] Remove support for Windows 7, 8, and 8.1 - #16566"
|
||||
],
|
||||
"3.2.4-beta1": [
|
||||
"[Fixed] Entering in double forward slash does not target directory in cloning dialog - #15842. Thanks @IgnazioGul!",
|
||||
"[Fixed] In the \"No Repositories\" screen, controls at the bottom stay inside window when it is resized - #16502. Thanks @samuelko123!",
|
||||
"[Fixed] Link to editor settings on the tutorial screen - #16636. Thanks @IgnazioGul!",
|
||||
"[Fixed] Close Squash Commit Message dialog on squash start - #16605",
|
||||
"[Fixed] Multi-commit diffing produces the same results whether you select up to down or down to up - #15883",
|
||||
"[Fixed] The misattributed commit avatar popover no longer causes the changes list to have scrollbars - #16684",
|
||||
"[Fixed] Autocompletion list is always visible regardless of its position on the screen - #16650 #16609",
|
||||
"[Improved] Improve screen reader support of the \"Create Alias\" dialog - #16802",
|
||||
"[Improved] Screen readers announce the number of results in filtered lists (like repositories, branches or pull requests) - #16779",
|
||||
"[Improved] Screen readers announce expanded/collapsed state of dropdowns - #16781",
|
||||
"[Improved] The context menu for a branches list items can be invoked by keyboard shortcuts - #16760",
|
||||
"[Improved] The context menu for a repository list item can be invoked by keyboard shortcuts - #16758",
|
||||
"[Improved] Make floating elements more responsive as the window or the UI are resized - #16717",
|
||||
"[Improved] Adds committing avatar popover to see git configuration and ability to open git configuration settings - #16640",
|
||||
"[Improved] Password inputs have a visibility toggle. - #16714",
|
||||
"[Improved] Welcome flow screen change in context is announced - #16698",
|
||||
"[Improved] Focus the sign in with browser button on opening the enterprise server login screen - #16706",
|
||||
"[Improved] Show the remote branch name if it does not match the local branch name - #13591. Thanks @samuelko123!",
|
||||
"[Improved] Reduce retries of avatars that fail to load - #16592",
|
||||
"[Removed] Remove support for Windows 7, 8, and 8.1 - #16566"
|
||||
],
|
||||
"3.2.3": [
|
||||
"[New] Add fetch and force-push actions in a dropdown as an alternative to the main Pull/Push/Publish action button - #15907",
|
||||
"[New] Get notified when someone comments your pull requests - #16226",
|
||||
|
|
Loading…
Reference in a new issue