mirror of
https://github.com/desktop/desktop
synced 2024-10-05 23:59:33 +00:00
Merge branch 'development' into releases/3.0.8
This commit is contained in:
commit
2ae90157d1
|
@ -15,6 +15,7 @@ extends:
|
|||
- prettier/react
|
||||
- plugin:@typescript-eslint/recommended
|
||||
- prettier/@typescript-eslint
|
||||
- plugin:github/react
|
||||
|
||||
rules:
|
||||
##########
|
||||
|
@ -29,6 +30,7 @@ rules:
|
|||
###########
|
||||
# PLUGINS #
|
||||
###########
|
||||
|
||||
# TYPESCRIPT
|
||||
'@typescript-eslint/naming-convention':
|
||||
- error
|
||||
|
|
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
|
@ -16,17 +16,14 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
node: [16.13.0]
|
||||
os: [macos-10.15, windows-2019]
|
||||
os: [macos-11, windows-2019]
|
||||
arch: [x64, arm64]
|
||||
include:
|
||||
- os: macos-10.15
|
||||
- os: macos-11
|
||||
friendlyName: macOS
|
||||
- os: windows-2019
|
||||
friendlyName: Windows
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
# Needed for macOS arm64 until hosted macos-11.0 runners become available
|
||||
SDKROOT: /Library/Developer/CommandLineTools/SDKs/MacOSX11.1.sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
|
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@v4.0.4
|
||||
uses: peter-evans/create-pull-request@v4.1.1
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test')
|
||||
with:
|
||||
|
|
2
.markdownlint.js
Normal file
2
.markdownlint.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
const markdownlintGitHub = require('@github/markdownlint-github')
|
||||
module.exports = markdownlintGitHub.init()
|
|
@ -30,7 +30,7 @@
|
|||
"desktop-trampoline": "desktop/desktop-trampoline#v0.9.8",
|
||||
"dexie": "^3.2.2",
|
||||
"dompurify": "^2.3.3",
|
||||
"dugite": "^1.110.0",
|
||||
"dugite": "^2.0.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"event-kit": "^2.0.0",
|
||||
"focus-trap-react": "^8.1.0",
|
||||
|
|
|
@ -201,7 +201,9 @@ export class CrashApp extends React.Component<ICrashAppProps, ICrashAppState> {
|
|||
}
|
||||
|
||||
private renderBackgroundGraphics() {
|
||||
return <img className="background-graphic-bottom" src={BottomImageUri} />
|
||||
return (
|
||||
<img className="background-graphic-bottom" alt="" src={BottomImageUri} />
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -31,6 +31,10 @@ const editors: IDarwinExternalEditor[] = [
|
|||
name: 'MacVim',
|
||||
bundleIdentifiers: ['org.vim.MacVim'],
|
||||
},
|
||||
{
|
||||
name: 'Neovide',
|
||||
bundleIdentifiers: ['com.neovide.neovide'],
|
||||
},
|
||||
{
|
||||
name: 'Visual Studio Code',
|
||||
bundleIdentifiers: ['com.microsoft.VSCode'],
|
||||
|
@ -132,6 +136,14 @@ const editors: IDarwinExternalEditor[] = [
|
|||
name: 'Nova',
|
||||
bundleIdentifiers: ['com.panic.Nova'],
|
||||
},
|
||||
{
|
||||
name: 'Emacs',
|
||||
bundleIdentifiers: ['org.gnu.Emacs'],
|
||||
},
|
||||
{
|
||||
name: 'Lite XL',
|
||||
bundleIdentifiers: ['com.lite-xl'],
|
||||
},
|
||||
]
|
||||
|
||||
async function findApplication(
|
||||
|
|
|
@ -58,6 +58,10 @@ const editors: ILinuxExternalEditor[] = [
|
|||
name: 'Code',
|
||||
paths: ['/usr/bin/io.elementary.code'],
|
||||
},
|
||||
{
|
||||
name: 'Lite XL',
|
||||
paths: ['/usr/bin/lite-xl'],
|
||||
},
|
||||
]
|
||||
|
||||
async function getAvailablePath(paths: string[]): Promise<string | null> {
|
||||
|
|
|
@ -30,7 +30,7 @@ function enableBetaFeatures(): boolean {
|
|||
|
||||
/** Should git pass `--recurse-submodules` when performing operations? */
|
||||
export function enableRecurseSubmodulesFlag(): boolean {
|
||||
return enableBetaFeatures()
|
||||
return true
|
||||
}
|
||||
|
||||
export function enableReadmeOverwriteWarning(): boolean {
|
||||
|
@ -102,3 +102,8 @@ export function enablePullRequestQuickView(): boolean {
|
|||
export function enableMultiCommitDiffs(): boolean {
|
||||
return enableBetaFeatures()
|
||||
}
|
||||
|
||||
/** Should we enable the new interstitial for submodule diffs? */
|
||||
export function enableSubmoduleDiff(): boolean {
|
||||
return enableBetaFeatures()
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ export async function applyPatchToIndex(
|
|||
const { kind } = diff
|
||||
switch (diff.kind) {
|
||||
case DiffType.Binary:
|
||||
case DiffType.Submodule:
|
||||
case DiffType.Image:
|
||||
throw new Error(
|
||||
`Can't create partial commit in binary file: ${file.path}`
|
||||
|
|
|
@ -161,7 +161,7 @@ export async function git(
|
|||
// from a terminal or if the system environment variables
|
||||
// have TERM set Git won't consider us as a smart terminal.
|
||||
// See https://github.com/git/git/blob/a7312d1a2/editor.c#L11-L15
|
||||
opts.env = { TERM: 'dumb', ...combinedEnv } as Object
|
||||
opts.env = { TERM: 'dumb', ...combinedEnv } as object
|
||||
|
||||
const commandName = `${name}: git ${args.join(' ')}`
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
FileChange,
|
||||
AppFileStatusKind,
|
||||
CommittedFileChange,
|
||||
SubmoduleStatus,
|
||||
} from '../../models/status'
|
||||
import {
|
||||
DiffType,
|
||||
|
@ -18,7 +19,6 @@ import {
|
|||
LineEndingsChange,
|
||||
parseLineEndingText,
|
||||
ILargeTextDiff,
|
||||
IUnrenderableDiff,
|
||||
} from '../../models/diff'
|
||||
|
||||
import { spawnAndComplete } from './spawn'
|
||||
|
@ -32,6 +32,8 @@ import { git } from './core'
|
|||
import { NullTreeSHA } from './diff-index'
|
||||
import { GitError } from 'dugite'
|
||||
import { parseRawLogWithNumstat } from './log'
|
||||
import { getConfigValue } from './config'
|
||||
import { getMergeBase } from './merge'
|
||||
|
||||
/**
|
||||
* V8 has a limit on the size of string it can create (~256MB), and unless we want to
|
||||
|
@ -138,6 +140,50 @@ export async function getCommitDiff(
|
|||
return buildDiff(output, repository, file, commitish)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the diff between two branches with --merge-base for a file
|
||||
* (Show what would be the result of merge)
|
||||
*/
|
||||
export async function getBranchMergeBaseDiff(
|
||||
repository: Repository,
|
||||
file: FileChange,
|
||||
baseBranchName: string,
|
||||
comparisonBranchName: string,
|
||||
hideWhitespaceInDiff: boolean = false,
|
||||
latestCommit: string
|
||||
): Promise<IDiff> {
|
||||
const args = [
|
||||
'diff',
|
||||
'--merge-base',
|
||||
baseBranchName,
|
||||
comparisonBranchName,
|
||||
...(hideWhitespaceInDiff ? ['-w'] : []),
|
||||
'--patch-with-raw',
|
||||
'-z',
|
||||
'--no-color',
|
||||
'--',
|
||||
file.path,
|
||||
]
|
||||
|
||||
if (
|
||||
file.status.kind === AppFileStatusKind.Renamed ||
|
||||
file.status.kind === AppFileStatusKind.Copied
|
||||
) {
|
||||
args.push(file.status.oldPath)
|
||||
}
|
||||
|
||||
const result = await git(args, repository.path, 'getBranchMergeBaseDiff', {
|
||||
maxBuffer: Infinity,
|
||||
})
|
||||
|
||||
return buildDiff(
|
||||
Buffer.from(result.combinedOutput),
|
||||
repository,
|
||||
file,
|
||||
latestCommit
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the difference between two commits for a file
|
||||
*
|
||||
|
@ -200,6 +246,52 @@ export async function getCommitRangeDiff(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the files that were changed for the merge base comparison of two branches.
|
||||
* (What would be the result of a merge)
|
||||
*/
|
||||
export async function getBranchMergeBaseChangedFiles(
|
||||
repository: Repository,
|
||||
baseBranchName: string,
|
||||
comparisonBranchName: string,
|
||||
latestComparisonBranchCommitRef: string
|
||||
): Promise<{
|
||||
files: ReadonlyArray<CommittedFileChange>
|
||||
linesAdded: number
|
||||
linesDeleted: number
|
||||
}> {
|
||||
const baseArgs = [
|
||||
'diff',
|
||||
'--merge-base',
|
||||
baseBranchName,
|
||||
comparisonBranchName,
|
||||
'-C',
|
||||
'-M',
|
||||
'-z',
|
||||
'--raw',
|
||||
'--numstat',
|
||||
'--',
|
||||
]
|
||||
|
||||
const result = await git(
|
||||
baseArgs,
|
||||
repository.path,
|
||||
'getBranchMergeBaseChangedFiles'
|
||||
)
|
||||
|
||||
const mergeBaseCommit = await getMergeBase(
|
||||
repository,
|
||||
baseBranchName,
|
||||
comparisonBranchName
|
||||
)
|
||||
|
||||
return parseRawLogWithNumstat(
|
||||
result.combinedOutput,
|
||||
`${latestComparisonBranchCommitRef}`,
|
||||
mergeBaseCommit ?? NullTreeSHA
|
||||
)
|
||||
}
|
||||
|
||||
export async function getCommitRangeChangedFiles(
|
||||
repository: Repository,
|
||||
shas: ReadonlyArray<string>,
|
||||
|
@ -268,10 +360,14 @@ export async function getWorkingDirectoryDiff(
|
|||
'--no-color',
|
||||
]
|
||||
const successExitCodes = new Set([0])
|
||||
const isSubmodule = file.status.submoduleStatus !== undefined
|
||||
|
||||
// For added submodules, we'll use the "default" parameters, which are able
|
||||
// to output the submodule commit.
|
||||
if (
|
||||
file.status.kind === AppFileStatusKind.New ||
|
||||
file.status.kind === AppFileStatusKind.Untracked
|
||||
!isSubmodule &&
|
||||
(file.status.kind === AppFileStatusKind.New ||
|
||||
file.status.kind === AppFileStatusKind.Untracked)
|
||||
) {
|
||||
// `git diff --no-index` seems to emulate the exit codes from `diff` irrespective of
|
||||
// whether you set --exit-code
|
||||
|
@ -480,16 +576,71 @@ function diffFromRawDiffOutput(output: Buffer): IRawDiff {
|
|||
return parser.parse(forceUnwrap(`Invalid diff output`, pieces.at(-1)))
|
||||
}
|
||||
|
||||
function buildDiff(
|
||||
async function buildSubmoduleDiff(
|
||||
buffer: Buffer,
|
||||
repository: Repository,
|
||||
file: FileChange,
|
||||
status: SubmoduleStatus
|
||||
): Promise<IDiff> {
|
||||
const path = file.path
|
||||
const fullPath = Path.join(repository.path, path)
|
||||
const url = await getConfigValue(repository, `submodule.${path}.url`, true)
|
||||
|
||||
let oldSHA = null
|
||||
let newSHA = null
|
||||
|
||||
if (
|
||||
status.commitChanged ||
|
||||
file.status.kind === AppFileStatusKind.New ||
|
||||
file.status.kind === AppFileStatusKind.Deleted
|
||||
) {
|
||||
const diff = buffer.toString('utf-8')
|
||||
const lines = diff.split('\n')
|
||||
const baseRegex = 'Subproject commit ([^-]+)(-dirty)?$'
|
||||
const oldSHARegex = new RegExp('-' + baseRegex)
|
||||
const newSHARegex = new RegExp('\\+' + baseRegex)
|
||||
const lineMatch = (regex: RegExp) =>
|
||||
lines
|
||||
.flatMap(line => {
|
||||
const match = line.match(regex)
|
||||
return match ? match[1] : []
|
||||
})
|
||||
.at(0) ?? null
|
||||
|
||||
oldSHA = lineMatch(oldSHARegex)
|
||||
newSHA = lineMatch(newSHARegex)
|
||||
}
|
||||
|
||||
return {
|
||||
kind: DiffType.Submodule,
|
||||
fullPath,
|
||||
path,
|
||||
url,
|
||||
status,
|
||||
oldSHA,
|
||||
newSHA,
|
||||
}
|
||||
}
|
||||
|
||||
async function buildDiff(
|
||||
buffer: Buffer,
|
||||
repository: Repository,
|
||||
file: FileChange,
|
||||
oldestCommitish: string,
|
||||
lineEndingsChange?: LineEndingsChange
|
||||
): Promise<IDiff> {
|
||||
if (file.status.submoduleStatus !== undefined) {
|
||||
return buildSubmoduleDiff(
|
||||
buffer,
|
||||
repository,
|
||||
file,
|
||||
file.status.submoduleStatus
|
||||
)
|
||||
}
|
||||
|
||||
if (!isValidBuffer(buffer)) {
|
||||
// the buffer's diff is too large to be renderable in the UI
|
||||
return Promise.resolve<IUnrenderableDiff>({ kind: DiffType.Unrenderable })
|
||||
return { kind: DiffType.Unrenderable }
|
||||
}
|
||||
|
||||
const diff = diffFromRawDiffOutput(buffer)
|
||||
|
@ -507,7 +658,7 @@ function buildDiff(
|
|||
hasHiddenBidiChars: diff.hasHiddenBidiChars,
|
||||
}
|
||||
|
||||
return Promise.resolve(largeTextDiff)
|
||||
return largeTextDiff
|
||||
}
|
||||
|
||||
return convertDiff(repository, file, diff, oldestCommitish, lineEndingsChange)
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
CopiedOrRenamedFileStatus,
|
||||
UntrackedFileStatus,
|
||||
AppFileStatus,
|
||||
SubmoduleStatus,
|
||||
} from '../../models/status'
|
||||
import { Repository } from '../../models/repository'
|
||||
import { Commit } from '../../models/commit'
|
||||
|
@ -15,47 +16,82 @@ import { getCaptures } from '../helpers/regex'
|
|||
import { createLogParser } from './git-delimiter-parser'
|
||||
import { revRange } from '.'
|
||||
import { forceUnwrap } from '../fatal-error'
|
||||
import { enableSubmoduleDiff } from '../feature-flag'
|
||||
|
||||
// File mode 160000 is used by git specifically for submodules:
|
||||
// https://github.com/git/git/blob/v2.37.3/cache.h#L62-L69
|
||||
const SubmoduleFileMode = '160000'
|
||||
|
||||
function mapSubmoduleStatusFileModes(
|
||||
status: string,
|
||||
srcMode: string,
|
||||
dstMode: string
|
||||
): SubmoduleStatus | undefined {
|
||||
if (!enableSubmoduleDiff()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return srcMode === SubmoduleFileMode &&
|
||||
dstMode === SubmoduleFileMode &&
|
||||
status === 'M'
|
||||
? {
|
||||
commitChanged: true,
|
||||
untrackedChanges: false,
|
||||
modifiedChanges: false,
|
||||
}
|
||||
: (srcMode === SubmoduleFileMode && status === 'D') ||
|
||||
(dstMode === SubmoduleFileMode && status === 'A')
|
||||
? {
|
||||
commitChanged: false,
|
||||
untrackedChanges: false,
|
||||
modifiedChanges: false,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the raw status text from Git to an app-friendly value
|
||||
* shamelessly borrowed from GitHub Desktop (Windows)
|
||||
*/
|
||||
export function mapStatus(
|
||||
function mapStatus(
|
||||
rawStatus: string,
|
||||
oldPath?: string
|
||||
oldPath: string | undefined,
|
||||
srcMode: string,
|
||||
dstMode: string
|
||||
): PlainFileStatus | CopiedOrRenamedFileStatus | UntrackedFileStatus {
|
||||
const status = rawStatus.trim()
|
||||
const submoduleStatus = mapSubmoduleStatusFileModes(status, srcMode, dstMode)
|
||||
|
||||
if (status === 'M') {
|
||||
return { kind: AppFileStatusKind.Modified }
|
||||
return { kind: AppFileStatusKind.Modified, submoduleStatus }
|
||||
} // modified
|
||||
if (status === 'A') {
|
||||
return { kind: AppFileStatusKind.New }
|
||||
return { kind: AppFileStatusKind.New, submoduleStatus }
|
||||
} // added
|
||||
if (status === '?') {
|
||||
return { kind: AppFileStatusKind.Untracked }
|
||||
return { kind: AppFileStatusKind.Untracked, submoduleStatus }
|
||||
} // untracked
|
||||
if (status === 'D') {
|
||||
return { kind: AppFileStatusKind.Deleted }
|
||||
return { kind: AppFileStatusKind.Deleted, submoduleStatus }
|
||||
} // deleted
|
||||
if (status === 'R' && oldPath != null) {
|
||||
return { kind: AppFileStatusKind.Renamed, oldPath }
|
||||
return { kind: AppFileStatusKind.Renamed, oldPath, submoduleStatus }
|
||||
} // renamed
|
||||
if (status === 'C' && oldPath != null) {
|
||||
return { kind: AppFileStatusKind.Copied, oldPath }
|
||||
return { kind: AppFileStatusKind.Copied, oldPath, submoduleStatus }
|
||||
} // copied
|
||||
|
||||
// git log -M --name-status will return a RXXX - where XXX is a percentage
|
||||
if (status.match(/R[0-9]+/) && oldPath != null) {
|
||||
return { kind: AppFileStatusKind.Renamed, oldPath }
|
||||
return { kind: AppFileStatusKind.Renamed, oldPath, submoduleStatus }
|
||||
}
|
||||
|
||||
// git log -C --name-status will return a CXXX - where XXX is a percentage
|
||||
if (status.match(/C[0-9]+/) && oldPath != null) {
|
||||
return { kind: AppFileStatusKind.Copied, oldPath }
|
||||
return { kind: AppFileStatusKind.Copied, oldPath, submoduleStatus }
|
||||
}
|
||||
|
||||
return { kind: AppFileStatusKind.Modified }
|
||||
return { kind: AppFileStatusKind.Modified, submoduleStatus }
|
||||
}
|
||||
|
||||
const isCopyOrRename = (
|
||||
|
@ -232,7 +268,19 @@ export function parseRawLogWithNumstat(
|
|||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
const line = lines[i]
|
||||
if (line.startsWith(':')) {
|
||||
const status = forceUnwrap('Invalid log output', line.split(' ').at(-1))
|
||||
const lineComponents = line.split(' ')
|
||||
const srcMode = forceUnwrap(
|
||||
'Invalid log output (srcMode)',
|
||||
lineComponents[0]?.replace(':', '')
|
||||
)
|
||||
const dstMode = forceUnwrap(
|
||||
'Invalid log output (dstMode)',
|
||||
lineComponents[1]
|
||||
)
|
||||
const status = forceUnwrap(
|
||||
'Invalid log output (status)',
|
||||
lineComponents.at(-1)
|
||||
)
|
||||
const oldPath = /^R|C/.test(status)
|
||||
? forceUnwrap('Missing old path', lines.at(++i))
|
||||
: undefined
|
||||
|
@ -242,7 +290,7 @@ export function parseRawLogWithNumstat(
|
|||
files.push(
|
||||
new CommittedFileChange(
|
||||
path,
|
||||
mapStatus(status, oldPath),
|
||||
mapStatus(status, oldPath, srcMode, dstMode),
|
||||
sha,
|
||||
parentCommitish
|
||||
)
|
||||
|
|
|
@ -104,7 +104,10 @@ function parseConflictedState(
|
|||
conflictDetails.conflictCountsByPath.get(path) || 0,
|
||||
}
|
||||
} else {
|
||||
return { kind: AppFileStatusKind.Conflicted, entry }
|
||||
return {
|
||||
kind: AppFileStatusKind.Conflicted,
|
||||
entry,
|
||||
}
|
||||
}
|
||||
}
|
||||
case UnmergedEntrySummary.BothModified: {
|
||||
|
@ -140,18 +143,38 @@ function convertToAppStatus(
|
|||
if (entry.kind === 'ordinary') {
|
||||
switch (entry.type) {
|
||||
case 'added':
|
||||
return { kind: AppFileStatusKind.New }
|
||||
return {
|
||||
kind: AppFileStatusKind.New,
|
||||
submoduleStatus: entry.submoduleStatus,
|
||||
}
|
||||
case 'modified':
|
||||
return { kind: AppFileStatusKind.Modified }
|
||||
return {
|
||||
kind: AppFileStatusKind.Modified,
|
||||
submoduleStatus: entry.submoduleStatus,
|
||||
}
|
||||
case 'deleted':
|
||||
return { kind: AppFileStatusKind.Deleted }
|
||||
return {
|
||||
kind: AppFileStatusKind.Deleted,
|
||||
submoduleStatus: entry.submoduleStatus,
|
||||
}
|
||||
}
|
||||
} else if (entry.kind === 'copied' && oldPath != null) {
|
||||
return { kind: AppFileStatusKind.Copied, oldPath }
|
||||
return {
|
||||
kind: AppFileStatusKind.Copied,
|
||||
oldPath,
|
||||
submoduleStatus: entry.submoduleStatus,
|
||||
}
|
||||
} else if (entry.kind === 'renamed' && oldPath != null) {
|
||||
return { kind: AppFileStatusKind.Renamed, oldPath }
|
||||
return {
|
||||
kind: AppFileStatusKind.Renamed,
|
||||
oldPath,
|
||||
submoduleStatus: entry.submoduleStatus,
|
||||
}
|
||||
} else if (entry.kind === 'untracked') {
|
||||
return { kind: AppFileStatusKind.Untracked }
|
||||
return {
|
||||
kind: AppFileStatusKind.Untracked,
|
||||
submoduleStatus: entry.submoduleStatus,
|
||||
}
|
||||
} else if (entry.kind === 'conflicted') {
|
||||
return parseConflictedState(entry, path, conflictDetails)
|
||||
}
|
||||
|
@ -270,7 +293,7 @@ function buildStatusMap(
|
|||
entry: IStatusEntry,
|
||||
conflictDetails: ConflictFilesDetails
|
||||
): Map<string, WorkingDirectoryFileChange> {
|
||||
const status = mapStatus(entry.statusCode)
|
||||
const status = mapStatus(entry.statusCode, entry.submoduleStatusCode)
|
||||
|
||||
if (status.kind === 'ordinary') {
|
||||
// when a file is added in the index but then removed in the working
|
||||
|
@ -300,7 +323,14 @@ function buildStatusMap(
|
|||
entry.oldPath
|
||||
)
|
||||
|
||||
const selection = DiffSelection.fromInitialSelection(DiffSelectionType.All)
|
||||
const initialSelectionType =
|
||||
appStatus.kind === AppFileStatusKind.Modified &&
|
||||
appStatus.submoduleStatus !== undefined &&
|
||||
!appStatus.submoduleStatus.commitChanged
|
||||
? DiffSelectionType.None
|
||||
: DiffSelectionType.All
|
||||
|
||||
const selection = DiffSelection.fromInitialSelection(initialSelectionType)
|
||||
|
||||
files.set(
|
||||
entry.path,
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
/** Create a copy of an object by merging it with a subset of its properties. */
|
||||
export function merge<T, K extends keyof T>(obj: T, subset: Pick<T, K>): T {
|
||||
export function merge<T extends {}, K extends keyof T>(
|
||||
obj: T | null | undefined,
|
||||
subset: Pick<T, K>
|
||||
): T {
|
||||
const copy = Object.assign({}, obj)
|
||||
for (const k in subset) {
|
||||
copy[k] = subset[k]
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import {
|
||||
FileEntry,
|
||||
GitStatusEntry,
|
||||
SubmoduleStatus,
|
||||
UnmergedEntrySummary,
|
||||
} from '../models/status'
|
||||
import { enableSubmoduleDiff } from './feature-flag'
|
||||
|
||||
type StatusItem = IStatusHeader | IStatusEntry
|
||||
|
||||
|
@ -21,6 +23,9 @@ export interface IStatusEntry {
|
|||
/** The two character long status code */
|
||||
readonly statusCode: string
|
||||
|
||||
/** The four character long submodule status code */
|
||||
readonly submoduleStatusCode: string
|
||||
|
||||
/** The original path in the case of a renamed file */
|
||||
readonly oldPath?: string
|
||||
}
|
||||
|
@ -102,7 +107,12 @@ function parseChangedEntry(field: string): IStatusEntry {
|
|||
throw new Error(`Failed to parse status line for changed entry`)
|
||||
}
|
||||
|
||||
return { kind: 'entry', statusCode: match[1], path: match[8] }
|
||||
return {
|
||||
kind: 'entry',
|
||||
statusCode: match[1],
|
||||
submoduleStatusCode: match[2],
|
||||
path: match[8],
|
||||
}
|
||||
}
|
||||
|
||||
// 2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path><sep><origPath>
|
||||
|
@ -126,7 +136,13 @@ function parsedRenamedOrCopiedEntry(
|
|||
)
|
||||
}
|
||||
|
||||
return { kind: 'entry', statusCode: match[1], oldPath, path: match[9] }
|
||||
return {
|
||||
kind: 'entry',
|
||||
statusCode: match[1],
|
||||
submoduleStatusCode: match[2],
|
||||
oldPath,
|
||||
path: match[9],
|
||||
}
|
||||
}
|
||||
|
||||
// u <xy> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>
|
||||
|
@ -144,6 +160,7 @@ function parseUnmergedEntry(field: string): IStatusEntry {
|
|||
return {
|
||||
kind: 'entry',
|
||||
statusCode: match[1],
|
||||
submoduleStatusCode: match[2],
|
||||
path: match[10],
|
||||
}
|
||||
}
|
||||
|
@ -155,198 +172,239 @@ function parseUntrackedEntry(field: string): IStatusEntry {
|
|||
// NOTE: We return ?? instead of ? here to play nice with mapStatus,
|
||||
// might want to consider changing this (and mapStatus) in the future.
|
||||
statusCode: '??',
|
||||
submoduleStatusCode: '????',
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
function mapSubmoduleStatus(
|
||||
submoduleStatusCode: string
|
||||
): SubmoduleStatus | undefined {
|
||||
if (!enableSubmoduleDiff() || !submoduleStatusCode.startsWith('S')) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
commitChanged: submoduleStatusCode[1] === 'C',
|
||||
modifiedChanges: submoduleStatusCode[2] === 'M',
|
||||
untrackedChanges: submoduleStatusCode[3] === 'U',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the raw status text from Git to a structure we can work with in the app.
|
||||
*/
|
||||
export function mapStatus(status: string): FileEntry {
|
||||
if (status === '??') {
|
||||
return { kind: 'untracked' }
|
||||
export function mapStatus(
|
||||
statusCode: string,
|
||||
submoduleStatusCode: string
|
||||
): FileEntry {
|
||||
const submoduleStatus = mapSubmoduleStatus(submoduleStatusCode)
|
||||
|
||||
if (statusCode === '??') {
|
||||
return { kind: 'untracked', submoduleStatus }
|
||||
}
|
||||
|
||||
if (status === '.M') {
|
||||
if (statusCode === '.M') {
|
||||
return {
|
||||
kind: 'ordinary',
|
||||
type: 'modified',
|
||||
index: GitStatusEntry.Unchanged,
|
||||
workingTree: GitStatusEntry.Modified,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'M.') {
|
||||
if (statusCode === 'M.') {
|
||||
return {
|
||||
kind: 'ordinary',
|
||||
type: 'modified',
|
||||
index: GitStatusEntry.Modified,
|
||||
workingTree: GitStatusEntry.Unchanged,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === '.A') {
|
||||
if (statusCode === '.A') {
|
||||
return {
|
||||
kind: 'ordinary',
|
||||
type: 'added',
|
||||
index: GitStatusEntry.Unchanged,
|
||||
workingTree: GitStatusEntry.Added,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'A.') {
|
||||
if (statusCode === 'A.') {
|
||||
return {
|
||||
kind: 'ordinary',
|
||||
type: 'added',
|
||||
index: GitStatusEntry.Added,
|
||||
workingTree: GitStatusEntry.Unchanged,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === '.D') {
|
||||
if (statusCode === '.D') {
|
||||
return {
|
||||
kind: 'ordinary',
|
||||
type: 'deleted',
|
||||
index: GitStatusEntry.Unchanged,
|
||||
workingTree: GitStatusEntry.Deleted,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'D.') {
|
||||
if (statusCode === 'D.') {
|
||||
return {
|
||||
kind: 'ordinary',
|
||||
type: 'deleted',
|
||||
index: GitStatusEntry.Deleted,
|
||||
workingTree: GitStatusEntry.Unchanged,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'R.') {
|
||||
if (statusCode === 'R.') {
|
||||
return {
|
||||
kind: 'renamed',
|
||||
index: GitStatusEntry.Renamed,
|
||||
workingTree: GitStatusEntry.Unchanged,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === '.R') {
|
||||
if (statusCode === '.R') {
|
||||
return {
|
||||
kind: 'renamed',
|
||||
index: GitStatusEntry.Unchanged,
|
||||
workingTree: GitStatusEntry.Renamed,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'C.') {
|
||||
if (statusCode === 'C.') {
|
||||
return {
|
||||
kind: 'copied',
|
||||
index: GitStatusEntry.Copied,
|
||||
workingTree: GitStatusEntry.Unchanged,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === '.C') {
|
||||
if (statusCode === '.C') {
|
||||
return {
|
||||
kind: 'copied',
|
||||
index: GitStatusEntry.Unchanged,
|
||||
workingTree: GitStatusEntry.Copied,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'AD') {
|
||||
if (statusCode === 'AD') {
|
||||
return {
|
||||
kind: 'ordinary',
|
||||
type: 'added',
|
||||
index: GitStatusEntry.Added,
|
||||
workingTree: GitStatusEntry.Deleted,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'AM') {
|
||||
if (statusCode === 'AM') {
|
||||
return {
|
||||
kind: 'ordinary',
|
||||
type: 'added',
|
||||
index: GitStatusEntry.Added,
|
||||
workingTree: GitStatusEntry.Modified,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'RM') {
|
||||
if (statusCode === 'RM') {
|
||||
return {
|
||||
kind: 'renamed',
|
||||
index: GitStatusEntry.Renamed,
|
||||
workingTree: GitStatusEntry.Modified,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'RD') {
|
||||
if (statusCode === 'RD') {
|
||||
return {
|
||||
kind: 'renamed',
|
||||
index: GitStatusEntry.Renamed,
|
||||
workingTree: GitStatusEntry.Deleted,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'DD') {
|
||||
if (statusCode === 'DD') {
|
||||
return {
|
||||
kind: 'conflicted',
|
||||
action: UnmergedEntrySummary.BothDeleted,
|
||||
us: GitStatusEntry.Deleted,
|
||||
them: GitStatusEntry.Deleted,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'AU') {
|
||||
if (statusCode === 'AU') {
|
||||
return {
|
||||
kind: 'conflicted',
|
||||
action: UnmergedEntrySummary.AddedByUs,
|
||||
us: GitStatusEntry.Added,
|
||||
them: GitStatusEntry.UpdatedButUnmerged,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'UD') {
|
||||
if (statusCode === 'UD') {
|
||||
return {
|
||||
kind: 'conflicted',
|
||||
action: UnmergedEntrySummary.DeletedByThem,
|
||||
us: GitStatusEntry.UpdatedButUnmerged,
|
||||
them: GitStatusEntry.Deleted,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'UA') {
|
||||
if (statusCode === 'UA') {
|
||||
return {
|
||||
kind: 'conflicted',
|
||||
action: UnmergedEntrySummary.AddedByThem,
|
||||
us: GitStatusEntry.UpdatedButUnmerged,
|
||||
them: GitStatusEntry.Added,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'DU') {
|
||||
if (statusCode === 'DU') {
|
||||
return {
|
||||
kind: 'conflicted',
|
||||
action: UnmergedEntrySummary.DeletedByUs,
|
||||
us: GitStatusEntry.Deleted,
|
||||
them: GitStatusEntry.UpdatedButUnmerged,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'AA') {
|
||||
if (statusCode === 'AA') {
|
||||
return {
|
||||
kind: 'conflicted',
|
||||
action: UnmergedEntrySummary.BothAdded,
|
||||
us: GitStatusEntry.Added,
|
||||
them: GitStatusEntry.Added,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'UU') {
|
||||
if (statusCode === 'UU') {
|
||||
return {
|
||||
kind: 'conflicted',
|
||||
action: UnmergedEntrySummary.BothModified,
|
||||
us: GitStatusEntry.UpdatedButUnmerged,
|
||||
them: GitStatusEntry.UpdatedButUnmerged,
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -354,5 +412,6 @@ export function mapStatus(status: string): FileEntry {
|
|||
return {
|
||||
kind: 'ordinary',
|
||||
type: 'modified',
|
||||
submoduleStatus,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ export class RepositoryIndicatorUpdater {
|
|||
|
||||
private clearRefreshTimeout() {
|
||||
if (this.refreshTimeoutId !== null) {
|
||||
window.clearTimeout()
|
||||
window.clearTimeout(this.refreshTimeoutId)
|
||||
this.refreshTimeoutId = null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { AccountsStore } from './accounts-store'
|
|||
import { getCommit } from '../git'
|
||||
import { GitHubRepository } from '../../models/github-repository'
|
||||
import { PullRequestCoordinator } from './pull-request-coordinator'
|
||||
import { Commit } from '../../models/commit'
|
||||
import { Commit, shortenSHA } from '../../models/commit'
|
||||
import {
|
||||
AliveStore,
|
||||
DesktopAliveEvent,
|
||||
|
@ -253,7 +253,7 @@ export class NotificationsStore {
|
|||
const pluralChecks =
|
||||
numberOfFailedChecks === 1 ? 'check was' : 'checks were'
|
||||
|
||||
const shortSHA = commitSHA.slice(0, 9)
|
||||
const shortSHA = shortenSHA(commitSHA)
|
||||
const title = 'Pull Request checks failed'
|
||||
const body = `${pullRequest.title} #${pullRequest.pullRequestNumber} (${shortSHA})\n${numberOfFailedChecks} ${pluralChecks} not successful.`
|
||||
const onClick = () => {
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
* variables.
|
||||
*/
|
||||
export async function withTrampolineEnv<T>(
|
||||
fn: (env: Object) => Promise<T>
|
||||
fn: (env: object) => Promise<T>
|
||||
): Promise<T> {
|
||||
const sshEnv = await getSSHEnvironment()
|
||||
|
||||
|
|
|
@ -2,6 +2,11 @@ import { CommitIdentity } from './commit-identity'
|
|||
import { ITrailer, isCoAuthoredByTrailer } from '../lib/git/interpret-trailers'
|
||||
import { GitAuthor } from './git-author'
|
||||
|
||||
/** Shortens a given SHA. */
|
||||
export function shortenSHA(sha: string) {
|
||||
return sha.slice(0, 9)
|
||||
}
|
||||
|
||||
/** Grouping of information required to create a commit */
|
||||
export interface ICommitContext {
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { DiffHunk } from './raw-diff'
|
||||
import { Image } from './image'
|
||||
import { SubmoduleStatus } from '../status'
|
||||
/**
|
||||
* V8 has a limit on the size of string it can create, and unless we want to
|
||||
* trigger an unhandled exception we need to do the encoding conversion by hand
|
||||
|
@ -87,6 +88,28 @@ export interface IBinaryDiff {
|
|||
readonly kind: DiffType.Binary
|
||||
}
|
||||
|
||||
export interface ISubmoduleDiff {
|
||||
readonly kind: DiffType.Submodule
|
||||
|
||||
/** Full path of the submodule */
|
||||
readonly fullPath: string
|
||||
|
||||
/** Path of the repository within its container repository */
|
||||
readonly path: string
|
||||
|
||||
/** URL of the submodule */
|
||||
readonly url: string | null
|
||||
|
||||
/** Status of the submodule */
|
||||
readonly status: SubmoduleStatus
|
||||
|
||||
/** Previous SHA of the submodule, or null if it hasn't changed */
|
||||
readonly oldSHA: string | null
|
||||
|
||||
/** New SHA of the submodule, or null if it hasn't changed */
|
||||
readonly newSHA: string | null
|
||||
}
|
||||
|
||||
export interface ILargeTextDiff extends ITextDiffData {
|
||||
readonly kind: DiffType.LargeText
|
||||
}
|
||||
|
@ -100,5 +123,6 @@ export type IDiff =
|
|||
| ITextDiff
|
||||
| IImageDiff
|
||||
| IBinaryDiff
|
||||
| ISubmoduleDiff
|
||||
| ILargeTextDiff
|
||||
| IUnrenderableDiff
|
||||
|
|
|
@ -34,6 +34,7 @@ export type PlainFileStatus = {
|
|||
| AppFileStatusKind.New
|
||||
| AppFileStatusKind.Modified
|
||||
| AppFileStatusKind.Deleted
|
||||
submoduleStatus?: SubmoduleStatus
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,6 +47,7 @@ export type PlainFileStatus = {
|
|||
export type CopiedOrRenamedFileStatus = {
|
||||
kind: AppFileStatusKind.Copied | AppFileStatusKind.Renamed
|
||||
oldPath: string
|
||||
submoduleStatus?: SubmoduleStatus
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,6 +58,7 @@ export type ConflictsWithMarkers = {
|
|||
kind: AppFileStatusKind.Conflicted
|
||||
entry: TextConflictEntry
|
||||
conflictMarkerCount: number
|
||||
submoduleStatus?: SubmoduleStatus
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,6 +68,7 @@ export type ConflictsWithMarkers = {
|
|||
export type ManualConflict = {
|
||||
kind: AppFileStatusKind.Conflicted
|
||||
entry: ManualConflictEntry
|
||||
submoduleStatus?: SubmoduleStatus
|
||||
}
|
||||
|
||||
/** Union of potential conflict scenarios the application should handle */
|
||||
|
@ -92,7 +96,10 @@ export function isManualConflict(
|
|||
}
|
||||
|
||||
/** Denotes an untracked file in the working directory) */
|
||||
export type UntrackedFileStatus = { kind: AppFileStatusKind.Untracked }
|
||||
export type UntrackedFileStatus = {
|
||||
kind: AppFileStatusKind.Untracked
|
||||
submoduleStatus?: SubmoduleStatus
|
||||
}
|
||||
|
||||
/** The union of potential states associated with a file change in Desktop */
|
||||
export type AppFileStatus =
|
||||
|
@ -101,6 +108,22 @@ export type AppFileStatus =
|
|||
| ConflictedFileStatus
|
||||
| UntrackedFileStatus
|
||||
|
||||
/** The status of a submodule */
|
||||
export type SubmoduleStatus = {
|
||||
/** Whether or not the submodule is pointing to a different commit */
|
||||
readonly commitChanged: boolean
|
||||
/**
|
||||
* Whether or not the submodule contains modified changes that haven't been
|
||||
* committed yet
|
||||
*/
|
||||
readonly modifiedChanges: boolean
|
||||
/**
|
||||
* Whether or not the submodule contains untracked changes that haven't been
|
||||
* committed yet
|
||||
*/
|
||||
readonly untrackedChanges: boolean
|
||||
}
|
||||
|
||||
/** The porcelain status for an ordinary changed entry */
|
||||
type OrdinaryEntry = {
|
||||
readonly kind: 'ordinary'
|
||||
|
@ -110,6 +133,8 @@ type OrdinaryEntry = {
|
|||
readonly index?: GitStatusEntry
|
||||
/** the status of the working tree for this entry (if known) */
|
||||
readonly workingTree?: GitStatusEntry
|
||||
/** the submodule status for this entry */
|
||||
readonly submoduleStatus?: SubmoduleStatus
|
||||
}
|
||||
|
||||
/** The porcelain status for a renamed or copied entry */
|
||||
|
@ -119,6 +144,8 @@ type RenamedOrCopiedEntry = {
|
|||
readonly index?: GitStatusEntry
|
||||
/** the status of the working tree for this entry (if known) */
|
||||
readonly workingTree?: GitStatusEntry
|
||||
/** the submodule status for this entry */
|
||||
readonly submoduleStatus?: SubmoduleStatus
|
||||
}
|
||||
|
||||
export enum UnmergedEntrySummary {
|
||||
|
@ -149,13 +176,18 @@ type TextConflictDetails =
|
|||
|
||||
type TextConflictEntry = {
|
||||
readonly kind: 'conflicted'
|
||||
/** the submodule status for this entry */
|
||||
readonly submoduleStatus?: SubmoduleStatus
|
||||
} & TextConflictDetails
|
||||
|
||||
/**
|
||||
* Valid Git index states where the user needs to choose one of `us` or `them`
|
||||
* in the app.
|
||||
*/
|
||||
type ManualConflictDetails =
|
||||
type ManualConflictDetails = {
|
||||
/** the submodule status for this entry */
|
||||
readonly submoduleStatus?: SubmoduleStatus
|
||||
} & (
|
||||
| {
|
||||
readonly action: UnmergedEntrySummary.BothAdded
|
||||
readonly us: GitStatusEntry.Added
|
||||
|
@ -191,9 +223,12 @@ type ManualConflictDetails =
|
|||
readonly us: GitStatusEntry.Deleted
|
||||
readonly them: GitStatusEntry.Deleted
|
||||
}
|
||||
)
|
||||
|
||||
type ManualConflictEntry = {
|
||||
readonly kind: 'conflicted'
|
||||
/** the submodule status for this entry */
|
||||
readonly submoduleStatus?: SubmoduleStatus
|
||||
} & ManualConflictDetails
|
||||
|
||||
/** The porcelain status for an unmerged entry */
|
||||
|
@ -202,6 +237,8 @@ export type UnmergedEntry = TextConflictEntry | ManualConflictEntry
|
|||
/** The porcelain status for an unmerged entry */
|
||||
type UntrackedEntry = {
|
||||
readonly kind: 'untracked'
|
||||
/** the submodule status for this entry */
|
||||
readonly submoduleStatus?: SubmoduleStatus
|
||||
}
|
||||
|
||||
/** The union of possible entries from the git status */
|
||||
|
|
|
@ -209,6 +209,7 @@ export class AppMenuBarButton extends React.Component<
|
|||
highlightAccessKey={this.props.highlightMenuAccessKey}
|
||||
renderAcceleratorText={false}
|
||||
renderSubMenuArrow={false}
|
||||
selected={false}
|
||||
/>
|
||||
</ToolbarDropdown>
|
||||
)
|
||||
|
|
|
@ -166,11 +166,12 @@ export class AppMenu extends React.Component<IAppMenuProps, {}> {
|
|||
}
|
||||
}
|
||||
|
||||
private onItemKeyDown = (
|
||||
private onPaneKeyDown = (
|
||||
depth: number,
|
||||
item: MenuItem,
|
||||
event: React.KeyboardEvent<any>
|
||||
event: React.KeyboardEvent<HTMLDivElement>
|
||||
) => {
|
||||
const { selectedItem } = this.props.state[depth]
|
||||
|
||||
if (event.key === 'ArrowLeft' || event.key === 'Escape') {
|
||||
this.clearExpandCollapseTimer()
|
||||
|
||||
|
@ -191,9 +192,9 @@ export class AppMenu extends React.Component<IAppMenuProps, {}> {
|
|||
this.clearExpandCollapseTimer()
|
||||
|
||||
// Open the submenu and select the first item
|
||||
if (item.type === 'submenuItem') {
|
||||
if (selectedItem?.type === 'submenuItem') {
|
||||
this.props.dispatcher.setAppMenuState(menu =>
|
||||
menu.withOpenedMenu(item, true)
|
||||
menu.withOpenedMenu(selectedItem, true)
|
||||
)
|
||||
this.focusPane = depth + 1
|
||||
event.preventDefault()
|
||||
|
@ -222,6 +223,12 @@ export class AppMenu extends React.Component<IAppMenuProps, {}> {
|
|||
}, expandCollapseTimeout)
|
||||
}
|
||||
|
||||
private onClearSelection = (depth: number) => {
|
||||
this.props.dispatcher.setAppMenuState(appMenu =>
|
||||
appMenu.withDeselectedMenu(this.props.state[depth])
|
||||
)
|
||||
}
|
||||
|
||||
private onSelectionChanged = (
|
||||
depth: number,
|
||||
item: MenuItem,
|
||||
|
@ -280,12 +287,6 @@ export class AppMenu extends React.Component<IAppMenuProps, {}> {
|
|||
}
|
||||
|
||||
private renderMenuPane(depth: number, menu: IMenu): JSX.Element {
|
||||
// NB: We use the menu id instead of depth as the key here to force
|
||||
// a new MenuPane instance and List. This is because we used dynamic
|
||||
// row heights and the react-virtualized Grid component isn't able to
|
||||
// recompute row heights accurately. Without this row indices which
|
||||
// previously held a separator item will retain that height and vice-
|
||||
// versa.
|
||||
// If the menu doesn't have an id it's the root menu
|
||||
const key = menu.id || '@'
|
||||
const className = menu.id ? menuPaneClassNameFromId(menu.id) : undefined
|
||||
|
@ -295,15 +296,15 @@ export class AppMenu extends React.Component<IAppMenuProps, {}> {
|
|||
key={key}
|
||||
ref={this.onMenuPaneRef}
|
||||
className={className}
|
||||
autoHeight={this.props.autoHeight}
|
||||
depth={depth}
|
||||
items={menu.items}
|
||||
selectedItem={menu.selectedItem}
|
||||
onItemClicked={this.onItemClicked}
|
||||
onMouseEnter={this.onPaneMouseEnter}
|
||||
onItemKeyDown={this.onItemKeyDown}
|
||||
onKeyDown={this.onPaneKeyDown}
|
||||
onSelectionChanged={this.onSelectionChanged}
|
||||
enableAccessKeyNavigation={this.props.enableAccessKeyNavigation}
|
||||
onClearSelection={this.onClearSelection}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -317,6 +318,7 @@ export class AppMenu extends React.Component<IAppMenuProps, {}> {
|
|||
this.paneRefs = this.paneRefs.slice(0, panes.length)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div id="app-menu-foldout" onKeyDown={this.onKeyDown}>
|
||||
{panes}
|
||||
</div>
|
||||
|
|
|
@ -34,6 +34,35 @@ interface IMenuListItemProps {
|
|||
* Defaults to true if not specified (i.e. undefined)
|
||||
*/
|
||||
readonly renderSubMenuArrow?: boolean
|
||||
|
||||
/**
|
||||
* Whether or not the menu item represented by this list item is the currently
|
||||
* selected menu item.
|
||||
*/
|
||||
readonly selected: boolean
|
||||
|
||||
/** Called when the user's pointer device enter the list item */
|
||||
readonly onMouseEnter?: (
|
||||
item: MenuItem,
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => void
|
||||
/** Called when the user's pointer device leaves the list item */
|
||||
readonly onMouseLeave?: (
|
||||
item: MenuItem,
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => void
|
||||
|
||||
/** Called when the user's pointer device clicks on the list item */
|
||||
readonly onClick?: (
|
||||
item: MenuItem,
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Whether the list item should steal focus when selected. Defaults to
|
||||
* false.
|
||||
*/
|
||||
readonly focusOnSelection?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,6 +78,8 @@ export function friendlyAcceleratorText(accelerator: string): string {
|
|||
}
|
||||
|
||||
export class MenuListItem extends React.Component<IMenuListItemProps, {}> {
|
||||
private wrapperRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
private getIcon(item: MenuItem): JSX.Element | null {
|
||||
if (item.type === 'checkbox' && item.checked) {
|
||||
return <Octicon className="icon" symbol={OcticonSymbol.check} />
|
||||
|
@ -59,6 +90,31 @@ export class MenuListItem extends React.Component<IMenuListItemProps, {}> {
|
|||
return null
|
||||
}
|
||||
|
||||
private onMouseEnter = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.props.onMouseEnter?.(this.props.item, event)
|
||||
}
|
||||
|
||||
private onMouseLeave = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.props.onMouseLeave?.(this.props.item, event)
|
||||
}
|
||||
|
||||
private onClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.props.onClick?.(this.props.item, event)
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.props.selected && this.props.focusOnSelection) {
|
||||
this.wrapperRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IMenuListItemProps) {
|
||||
const { focusOnSelection, selected } = this.props
|
||||
if (focusOnSelection && selected && !prevProps.selected) {
|
||||
this.wrapperRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const item = this.props.item
|
||||
|
||||
|
@ -83,19 +139,26 @@ export class MenuListItem extends React.Component<IMenuListItemProps, {}> {
|
|||
</div>
|
||||
) : null
|
||||
|
||||
const className = classNames(
|
||||
'menu-item',
|
||||
{ disabled: !item.enabled },
|
||||
{ checkbox: item.type === 'checkbox' },
|
||||
{ radio: item.type === 'radio' },
|
||||
{
|
||||
checked:
|
||||
(item.type === 'checkbox' || item.type === 'radio') && item.checked,
|
||||
}
|
||||
)
|
||||
const { type } = item
|
||||
|
||||
const className = classNames('menu-item', {
|
||||
disabled: !item.enabled,
|
||||
checkbox: type === 'checkbox',
|
||||
radio: type === 'radio',
|
||||
checked: (type === 'checkbox' || type === 'radio') && item.checked,
|
||||
selected: this.props.selected,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus
|
||||
<div
|
||||
className={className}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onClick={this.onClick}
|
||||
ref={this.wrapperRef}
|
||||
role="menuitem"
|
||||
>
|
||||
{this.getIcon(item)}
|
||||
<div className="label">
|
||||
<AccessText
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import * as React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { List, ClickSource, SelectionSource } from '../lib/list'
|
||||
import {
|
||||
ClickSource,
|
||||
findLastSelectableRow,
|
||||
findNextSelectableRow,
|
||||
IHoverSource,
|
||||
IKeyboardSource,
|
||||
IMouseClickSource,
|
||||
SelectionSource,
|
||||
} from '../lib/list'
|
||||
import {
|
||||
MenuItem,
|
||||
itemIsSelectable,
|
||||
findItemByAccessKey,
|
||||
} from '../../models/app-menu'
|
||||
import { MenuListItem } from './menu-list-item'
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
|
||||
interface IMenuPaneProps {
|
||||
/**
|
||||
|
@ -47,14 +56,13 @@ interface IMenuPaneProps {
|
|||
) => void
|
||||
|
||||
/**
|
||||
* A callback for when a keyboard key is pressed on a menu item. Note that
|
||||
* this only picks up on keyboard events received by a MenuItem and does
|
||||
* not cover keyboard events received on the MenuPane component itself.
|
||||
* Called when the user presses down on a key while focused on, or within, the
|
||||
* menu pane. Consumers should inspect isDefaultPrevented to determine whether
|
||||
* the event was handled by the menu pane or not.
|
||||
*/
|
||||
readonly onItemKeyDown?: (
|
||||
readonly onKeyDown?: (
|
||||
depth: number,
|
||||
item: MenuItem,
|
||||
event: React.KeyboardEvent<any>
|
||||
event: React.KeyboardEvent<HTMLDivElement>
|
||||
) => void
|
||||
|
||||
/**
|
||||
|
@ -77,115 +85,52 @@ interface IMenuPaneProps {
|
|||
readonly enableAccessKeyNavigation: boolean
|
||||
|
||||
/**
|
||||
* If true the MenuPane only takes up as much vertical space needed to
|
||||
* show all menu items. This does not affect maximum height, i.e. if the
|
||||
* visible menu items takes up more space than what is available the menu
|
||||
* will still overflow and be scrollable.
|
||||
*
|
||||
* @default false
|
||||
* Called to deselect the currently selected menu item (if any). This
|
||||
* will be called when the user's pointer device leaves a menu item.
|
||||
*/
|
||||
readonly autoHeight?: boolean
|
||||
readonly onClearSelection: (depth: number) => void
|
||||
}
|
||||
|
||||
interface IMenuPaneState {
|
||||
/**
|
||||
* A list of visible menu items that is to be rendered. This is a derivative
|
||||
* of the props items with invisible items filtered out.
|
||||
*/
|
||||
readonly items: ReadonlyArray<MenuItem>
|
||||
|
||||
/** The selected row index or -1 if no selection exists. */
|
||||
readonly selectedIndex: number
|
||||
}
|
||||
|
||||
const RowHeight = 30
|
||||
const SeparatorRowHeight = 10
|
||||
|
||||
function getSelectedIndex(
|
||||
selectedItem: MenuItem | undefined,
|
||||
items: ReadonlyArray<MenuItem>
|
||||
) {
|
||||
return selectedItem ? items.findIndex(i => i.id === selectedItem.id) : -1
|
||||
}
|
||||
|
||||
export function getListHeight(menuItems: ReadonlyArray<MenuItem>) {
|
||||
return menuItems.reduce((acc, item) => acc + getRowHeight(item), 0)
|
||||
}
|
||||
|
||||
export function getRowHeight(item: MenuItem) {
|
||||
if (!item.visible) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return item.type === 'separator' ? SeparatorRowHeight : RowHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a menu pane state given props. This is intentionally not
|
||||
* an instance member in order to avoid mistakenly using any other
|
||||
* input data or state than the received props.
|
||||
*/
|
||||
function createState(props: IMenuPaneProps): IMenuPaneState {
|
||||
const items = new Array<MenuItem>()
|
||||
const selectedItem = props.selectedItem
|
||||
|
||||
let selectedIndex = -1
|
||||
|
||||
// Filter out all invisible items and maintain the correct
|
||||
// selected index (if possible)
|
||||
for (let i = 0; i < props.items.length; i++) {
|
||||
const item = props.items[i]
|
||||
|
||||
if (item.visible) {
|
||||
items.push(item)
|
||||
if (item === selectedItem) {
|
||||
selectedIndex = items.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { items, selectedIndex }
|
||||
}
|
||||
|
||||
export class MenuPane extends React.Component<IMenuPaneProps, IMenuPaneState> {
|
||||
private list: List | null = null
|
||||
|
||||
public constructor(props: IMenuPaneProps) {
|
||||
super(props)
|
||||
this.state = createState(props)
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: IMenuPaneProps) {
|
||||
// No need to recreate the filtered list if it hasn't changed,
|
||||
// we only have to update the selected item
|
||||
if (this.props.items === nextProps.items) {
|
||||
// Has the selection changed?
|
||||
if (this.props.selectedItem !== nextProps.selectedItem) {
|
||||
const selectedIndex = getSelectedIndex(
|
||||
nextProps.selectedItem,
|
||||
this.state.items
|
||||
)
|
||||
this.setState({ selectedIndex })
|
||||
}
|
||||
} else {
|
||||
this.setState(createState(nextProps))
|
||||
}
|
||||
}
|
||||
|
||||
private onRowClick = (row: number, source: ClickSource) => {
|
||||
const item = this.state.items[row]
|
||||
export class MenuPane extends React.Component<IMenuPaneProps> {
|
||||
private paneRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
private onRowClick = (
|
||||
item: MenuItem,
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (item.type !== 'separator' && item.enabled) {
|
||||
const source: IMouseClickSource = { kind: 'mouseclick', event }
|
||||
this.props.onItemClicked(this.props.depth, item, source)
|
||||
}
|
||||
}
|
||||
|
||||
private onSelectedRowChanged = (row: number, source: SelectionSource) => {
|
||||
const item = this.state.items[row]
|
||||
this.props.onSelectionChanged(this.props.depth, item, source)
|
||||
private tryMoveSelection(
|
||||
direction: 'up' | 'down' | 'first' | 'last',
|
||||
source: ClickSource
|
||||
) {
|
||||
const { items, selectedItem } = this.props
|
||||
const row = selectedItem ? items.indexOf(selectedItem) : -1
|
||||
const count = items.length
|
||||
const selectable = (ix: number) => items[ix] && itemIsSelectable(items[ix])
|
||||
|
||||
let ix: number | null = null
|
||||
|
||||
if (direction === 'up' || direction === 'down') {
|
||||
ix = findNextSelectableRow(count, { direction, row }, selectable)
|
||||
} else if (direction === 'first' || direction === 'last') {
|
||||
const d = direction === 'first' ? 'up' : 'down'
|
||||
ix = findLastSelectableRow(d, count, selectable)
|
||||
}
|
||||
|
||||
if (ix !== null && items[ix] !== undefined) {
|
||||
this.props.onSelectionChanged(this.props.depth, items[ix], source)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private onKeyDown = (event: React.KeyboardEvent<any>) => {
|
||||
private onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
@ -195,103 +140,115 @@ export class MenuPane extends React.Component<IMenuPaneProps, IMenuPaneState> {
|
|||
return
|
||||
}
|
||||
|
||||
const source: IKeyboardSource = { kind: 'keyboard', event }
|
||||
const { selectedItem } = this.props
|
||||
const { key } = event
|
||||
|
||||
if (isSupportedKey(key)) {
|
||||
event.preventDefault()
|
||||
|
||||
if (key === 'ArrowUp' || key === 'ArrowDown') {
|
||||
this.tryMoveSelection(key === 'ArrowUp' ? 'up' : 'down', source)
|
||||
} else if (key === 'Home' || key === 'End') {
|
||||
const direction = key === 'Home' ? 'first' : 'last'
|
||||
this.tryMoveSelection(direction, source)
|
||||
} else if (key === 'Enter' || key === ' ') {
|
||||
if (selectedItem !== undefined) {
|
||||
this.props.onItemClicked(this.props.depth, selectedItem, source)
|
||||
}
|
||||
} else {
|
||||
assertNever(key, 'Unsupported key')
|
||||
}
|
||||
}
|
||||
|
||||
// If we weren't opened with the Alt key we ignore key presses other than
|
||||
// arrow keys and Enter/Space etc.
|
||||
if (!this.props.enableAccessKeyNavigation) {
|
||||
return
|
||||
if (this.props.enableAccessKeyNavigation) {
|
||||
// At this point the list will already have intercepted any arrow keys
|
||||
// and the list items themselves will have caught Enter/Space
|
||||
const item = findItemByAccessKey(event.key, this.props.items)
|
||||
if (item && itemIsSelectable(item)) {
|
||||
event.preventDefault()
|
||||
this.props.onSelectionChanged(this.props.depth, item, {
|
||||
kind: 'keyboard',
|
||||
event: event,
|
||||
})
|
||||
this.props.onItemClicked(this.props.depth, item, {
|
||||
kind: 'keyboard',
|
||||
event: event,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// At this point the list will already have intercepted any arrow keys
|
||||
// and the list items themselves will have caught Enter/Space
|
||||
const item = findItemByAccessKey(event.key, this.state.items)
|
||||
if (item && itemIsSelectable(item)) {
|
||||
event.preventDefault()
|
||||
this.props.onSelectionChanged(this.props.depth, item, {
|
||||
kind: 'keyboard',
|
||||
event: event,
|
||||
})
|
||||
this.props.onItemClicked(this.props.depth, item, {
|
||||
kind: 'keyboard',
|
||||
event: event,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private onRowKeyDown = (row: number, event: React.KeyboardEvent<any>) => {
|
||||
if (this.props.onItemKeyDown) {
|
||||
const item = this.state.items[row]
|
||||
this.props.onItemKeyDown(this.props.depth, item, event)
|
||||
}
|
||||
}
|
||||
|
||||
private canSelectRow = (row: number) => {
|
||||
const item = this.state.items[row]
|
||||
return itemIsSelectable(item)
|
||||
}
|
||||
|
||||
private onListRef = (list: List | null) => {
|
||||
this.list = list
|
||||
this.props.onKeyDown?.(this.props.depth, event)
|
||||
}
|
||||
|
||||
private onMouseEnter = (event: React.MouseEvent<any>) => {
|
||||
if (this.props.onMouseEnter) {
|
||||
this.props.onMouseEnter(this.props.depth)
|
||||
this.props.onMouseEnter?.(this.props.depth)
|
||||
}
|
||||
|
||||
private onRowMouseEnter = (
|
||||
item: MenuItem,
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (itemIsSelectable(item)) {
|
||||
const source: IHoverSource = { kind: 'hover', event }
|
||||
this.props.onSelectionChanged(this.props.depth, item, source)
|
||||
}
|
||||
}
|
||||
|
||||
private renderMenuItem = (row: number) => {
|
||||
const item = this.state.items[row]
|
||||
|
||||
return (
|
||||
<MenuListItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
highlightAccessKey={this.props.enableAccessKeyNavigation}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private rowHeight = (info: { index: number }) => {
|
||||
const item = this.state.items[info.index]
|
||||
return item.type === 'separator' ? SeparatorRowHeight : RowHeight
|
||||
private onRowMouseLeave = (
|
||||
item: MenuItem,
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (this.props.selectedItem === item) {
|
||||
this.props.onClearSelection(this.props.depth)
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const style: React.CSSProperties =
|
||||
this.props.autoHeight === true
|
||||
? { height: getListHeight(this.props.items) + 5, maxHeight: '100%' }
|
||||
: {}
|
||||
|
||||
const className = classNames('menu-pane', this.props.className)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={className}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onKeyDown={this.onKeyDown}
|
||||
style={style}
|
||||
ref={this.paneRef}
|
||||
tabIndex={-1}
|
||||
role="menu"
|
||||
>
|
||||
<List
|
||||
ref={this.onListRef}
|
||||
rowCount={this.state.items.length}
|
||||
rowHeight={this.rowHeight}
|
||||
rowRenderer={this.renderMenuItem}
|
||||
selectedRows={[this.state.selectedIndex]}
|
||||
onRowClick={this.onRowClick}
|
||||
onSelectedRowChanged={this.onSelectedRowChanged}
|
||||
canSelectRow={this.canSelectRow}
|
||||
onRowKeyDown={this.onRowKeyDown}
|
||||
invalidationProps={this.state.items}
|
||||
selectOnHover={true}
|
||||
ariaMode="menu"
|
||||
/>
|
||||
{this.props.items
|
||||
.filter(x => x.visible)
|
||||
.map((item, ix) => (
|
||||
<MenuListItem
|
||||
key={ix + item.id}
|
||||
item={item}
|
||||
highlightAccessKey={this.props.enableAccessKeyNavigation}
|
||||
selected={item.id === this.props.selectedItem?.id}
|
||||
onMouseEnter={this.onRowMouseEnter}
|
||||
onMouseLeave={this.onRowMouseLeave}
|
||||
onClick={this.onRowClick}
|
||||
focusOnSelection={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public focus() {
|
||||
if (this.list) {
|
||||
this.list.focus()
|
||||
}
|
||||
this.paneRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const supportedKeys = [
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'Home',
|
||||
'End',
|
||||
'Enter',
|
||||
' ',
|
||||
] as const
|
||||
const isSupportedKey = (key: string): key is typeof supportedKeys[number] =>
|
||||
(supportedKeys as readonly string[]).includes(key)
|
||||
|
|
|
@ -90,7 +90,7 @@ export class EmojiAutocompletionProvider
|
|||
|
||||
return (
|
||||
<div className="emoji" key={emoji}>
|
||||
<img className="icon" src={this.emoji.get(emoji)} />
|
||||
<img className="icon" src={this.emoji.get(emoji)} alt={emoji} />
|
||||
{this.renderHighlightedTitle(hit)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
import * as React from 'react'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
|
|
|
@ -131,6 +131,7 @@ export class BranchListItem extends React.Component<
|
|||
})
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
onContextMenu={this.onContextMenu}
|
||||
className={className}
|
||||
|
|
|
@ -258,6 +258,7 @@ export class BranchesContainer extends React.Component<
|
|||
const label = __DARWIN__ ? 'New Branch' : 'New branch'
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className="branches-list-item new-branch-drop"
|
||||
onMouseEnter={this.onMouseEnterNewBranchDrop}
|
||||
|
|
|
@ -19,7 +19,7 @@ export class NoBranches extends React.Component<INoBranchesProps> {
|
|||
if (this.props.canCreateNewBranch) {
|
||||
return (
|
||||
<div className="no-branches">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
<img src={BlankSlateImage} className="blankslate-image" alt="" />
|
||||
|
||||
<div className="title">Sorry, I can't find that branch</div>
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ export class NoPullRequests extends React.Component<INoPullRequestsProps, {}> {
|
|||
public render() {
|
||||
return (
|
||||
<div className="no-pull-requests">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
<img src={BlankSlateImage} className="blankslate-image" alt="" />
|
||||
{this.renderTitle()}
|
||||
{this.renderCallToAction()}
|
||||
</div>
|
||||
|
|
|
@ -77,6 +77,7 @@ export class PullRequestBadge extends React.Component<
|
|||
public render() {
|
||||
const ref = getPullRequestCommitRef(this.props.number)
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div className="pr-badge" onClick={this.onBadgeClick} ref={this.onRef}>
|
||||
<span className="number">#{this.props.number}</span>
|
||||
<CIStatus
|
||||
|
|
|
@ -126,6 +126,7 @@ export class PullRequestListItem extends React.Component<
|
|||
})
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={className}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
|
|
|
@ -43,16 +43,7 @@ export class ChangedFileDetails extends React.Component<
|
|||
<PathLabel path={this.props.path} status={this.props.status} />
|
||||
{this.renderDecorator()}
|
||||
|
||||
<DiffOptions
|
||||
sourceTab={RepositorySectionTab.Changes}
|
||||
onHideWhitespaceChangesChanged={
|
||||
this.props.onHideWhitespaceInDiffChanged
|
||||
}
|
||||
hideWhitespaceChanges={this.props.hideWhitespaceInDiff}
|
||||
onShowSideBySideDiffChanged={this.props.onShowSideBySideDiffChanged}
|
||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||
onDiffOptionsOpened={this.props.onDiffOptionsOpened}
|
||||
/>
|
||||
{this.renderDiffOptions()}
|
||||
|
||||
<Octicon
|
||||
symbol={iconForStatus(status)}
|
||||
|
@ -63,6 +54,25 @@ export class ChangedFileDetails extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private renderDiffOptions() {
|
||||
if (this.props.diff?.kind === DiffType.Submodule) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<DiffOptions
|
||||
sourceTab={RepositorySectionTab.Changes}
|
||||
onHideWhitespaceChangesChanged={
|
||||
this.props.onHideWhitespaceInDiffChanged
|
||||
}
|
||||
hideWhitespaceChanges={this.props.hideWhitespaceInDiff}
|
||||
onShowSideBySideDiffChanged={this.props.onShowSideBySideDiffChanged}
|
||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||
onDiffOptionsOpened={this.props.onDiffOptionsOpened}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderDecorator() {
|
||||
const diff = this.props.diff
|
||||
|
||||
|
|
|
@ -6,12 +6,14 @@ import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
|||
import { mapStatus } from '../../lib/status'
|
||||
import { WorkingDirectoryFileChange } from '../../models/status'
|
||||
import { TooltipDirection } from '../lib/tooltip'
|
||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||
|
||||
interface IChangedFileProps {
|
||||
readonly file: WorkingDirectoryFileChange
|
||||
readonly include: boolean | null
|
||||
readonly availableWidth: number
|
||||
readonly disableSelection: boolean
|
||||
readonly checkboxTooltip?: string
|
||||
readonly onIncludeChanged: (path: string, include: boolean) => void
|
||||
|
||||
/** Callback called when user right-clicks on an item */
|
||||
|
@ -39,7 +41,9 @@ export class ChangedFile extends React.Component<IChangedFileProps, {}> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { status, path } = this.props.file
|
||||
const { file, availableWidth, disableSelection, checkboxTooltip } =
|
||||
this.props
|
||||
const { status, path } = file
|
||||
const fileStatus = mapStatus(status)
|
||||
|
||||
const listItemPadding = 10 * 2
|
||||
|
@ -48,7 +52,7 @@ export class ChangedFile extends React.Component<IChangedFileProps, {}> {
|
|||
const filePadding = 5
|
||||
|
||||
const availablePathWidth =
|
||||
this.props.availableWidth -
|
||||
availableWidth -
|
||||
listItemPadding -
|
||||
checkboxWidth -
|
||||
filePadding -
|
||||
|
@ -56,15 +60,21 @@ export class ChangedFile extends React.Component<IChangedFileProps, {}> {
|
|||
|
||||
return (
|
||||
<div className="file" onContextMenu={this.onContextMenu}>
|
||||
<Checkbox
|
||||
// The checkbox doesn't need to be tab reachable since we emulate
|
||||
// checkbox behavior on the list item itself, ie hitting space bar
|
||||
// while focused on a row will toggle selection.
|
||||
tabIndex={-1}
|
||||
value={this.checkboxValue}
|
||||
onChange={this.handleCheckboxChange}
|
||||
disabled={this.props.disableSelection}
|
||||
/>
|
||||
<TooltippedContent
|
||||
tooltip={checkboxTooltip}
|
||||
direction={TooltipDirection.EAST}
|
||||
tagName="div"
|
||||
>
|
||||
<Checkbox
|
||||
// The checkbox doesn't need to be tab reachable since we emulate
|
||||
// checkbox behavior on the list item itself, ie hitting space bar
|
||||
// while focused on a row will toggle selection.
|
||||
tabIndex={-1}
|
||||
value={this.checkboxValue}
|
||||
onChange={this.handleCheckboxChange}
|
||||
disabled={disableSelection}
|
||||
/>
|
||||
</TooltippedContent>
|
||||
|
||||
<PathLabel
|
||||
path={path}
|
||||
|
|
|
@ -278,6 +278,18 @@ export class ChangesList extends React.Component<
|
|||
|
||||
const file = workingDirectory.files[row]
|
||||
const selection = file.selection.getSelectionType()
|
||||
const { submoduleStatus } = file.status
|
||||
|
||||
const isUncommittableSubmodule =
|
||||
submoduleStatus !== undefined &&
|
||||
file.status.kind === AppFileStatusKind.Modified &&
|
||||
!submoduleStatus.commitChanged
|
||||
|
||||
const isPartiallyCommittableSubmodule =
|
||||
submoduleStatus !== undefined &&
|
||||
(submoduleStatus.commitChanged ||
|
||||
file.status.kind === AppFileStatusKind.New) &&
|
||||
(submoduleStatus.modifiedChanges || submoduleStatus.untrackedChanges)
|
||||
|
||||
const includeAll =
|
||||
selection === DiffSelectionType.All
|
||||
|
@ -286,22 +298,31 @@ export class ChangesList extends React.Component<
|
|||
? false
|
||||
: null
|
||||
|
||||
const include =
|
||||
rebaseConflictState !== null
|
||||
? file.status.kind !== AppFileStatusKind.Untracked
|
||||
: includeAll
|
||||
const include = isUncommittableSubmodule
|
||||
? false
|
||||
: rebaseConflictState !== null
|
||||
? file.status.kind !== AppFileStatusKind.Untracked
|
||||
: includeAll
|
||||
|
||||
const disableSelection = isCommitting || rebaseConflictState !== null
|
||||
const disableSelection =
|
||||
isCommitting || rebaseConflictState !== null || isUncommittableSubmodule
|
||||
|
||||
const checkboxTooltip = isUncommittableSubmodule
|
||||
? 'This submodule change cannot be added to a commit in this repository because it contains changes that have not been committed.'
|
||||
: isPartiallyCommittableSubmodule
|
||||
? 'Only changes that have been committed within the submodule will be added to this repository. You need to commit any other modified or untracked changes in the submodule before including them in this repository.'
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<ChangedFile
|
||||
file={file}
|
||||
include={include}
|
||||
include={isPartiallyCommittableSubmodule && include ? null : include}
|
||||
key={file.id}
|
||||
onContextMenu={this.onItemContextMenu}
|
||||
onIncludeChanged={onIncludeChanged}
|
||||
availableWidth={availableWidth}
|
||||
disableSelection={disableSelection}
|
||||
checkboxTooltip={checkboxTooltip}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -852,6 +873,7 @@ export class ChangesList extends React.Component<
|
|||
)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/role-supports-aria-props
|
||||
<button
|
||||
className={className}
|
||||
onClick={this.onStashEntryClicked}
|
||||
|
|
|
@ -29,6 +29,9 @@ interface IChangesProps {
|
|||
*/
|
||||
readonly onOpenBinaryFile: (fullPath: string) => void
|
||||
|
||||
/** Called when the user requests to open a submodule. */
|
||||
readonly onOpenSubmodule: (fullPath: string) => void
|
||||
|
||||
/**
|
||||
* Called when the user is viewing an image diff and requests
|
||||
* to change the diff presentation mode.
|
||||
|
@ -121,6 +124,7 @@ export class Changes extends React.Component<IChangesProps, {}> {
|
|||
this.props.askForConfirmationOnDiscardChanges
|
||||
}
|
||||
onOpenBinaryFile={this.props.onOpenBinaryFile}
|
||||
onOpenSubmodule={this.props.onOpenSubmodule}
|
||||
onChangeImageDiffType={this.props.onChangeImageDiffType}
|
||||
onHideWhitespaceInDiffChanged={this.onHideWhitespaceInDiffChanged}
|
||||
/>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React from 'react'
|
||||
import { Select } from '../lib/select'
|
||||
import { Button } from '../lib/button'
|
||||
|
|
|
@ -766,6 +766,7 @@ export class CommitMessage extends React.Component<
|
|||
})
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Create commit"
|
||||
|
|
|
@ -18,7 +18,7 @@ export class MultipleSelection extends React.Component<
|
|||
public render() {
|
||||
return (
|
||||
<div className="panel blankslate" id="no-changes">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
<img src={BlankSlateImage} className="blankslate-image" alt="" />
|
||||
<div>{this.props.count} files selected</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -704,9 +704,9 @@ export class NoChanges extends React.Component<
|
|||
|
||||
public render() {
|
||||
return (
|
||||
<div id="no-changes">
|
||||
<div className="changes-interstitial">
|
||||
<div className="content">
|
||||
<div className="header">
|
||||
<div className="interstitial-header">
|
||||
<div className="text">
|
||||
<h1>No local changes</h1>
|
||||
<p>
|
||||
|
@ -714,7 +714,7 @@ export class NoChanges extends React.Component<
|
|||
some friendly suggestions for what to do next.
|
||||
</p>
|
||||
</div>
|
||||
<img src={PaperStackImage} className="blankslate-image" />
|
||||
<img src={PaperStackImage} className="blankslate-image" alt="" />
|
||||
</div>
|
||||
{this.renderActions()}
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import * as React from 'react'
|
||||
import { Octicon } from '../octicons'
|
||||
import classNames from 'classnames'
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import * as React from 'react'
|
||||
import { IRefCheck } from '../../lib/ci-checks/ci-checks'
|
||||
import { Octicon } from '../octicons'
|
||||
|
@ -209,6 +211,7 @@ export class CICheckRunListItem extends React.PureComponent<
|
|||
<div
|
||||
className={classes}
|
||||
onClick={this.toggleCheckRunExpansion}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={0}
|
||||
>
|
||||
{this.renderCheckStatusSymbol()}
|
||||
|
|
|
@ -242,7 +242,7 @@ export class CICheckRunPopover extends React.PureComponent<
|
|||
private renderCheckRunLoadings(): JSX.Element {
|
||||
return (
|
||||
<div className="loading-check-runs">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
<img src={BlankSlateImage} className="blankslate-image" alt="" />
|
||||
<div className="title">Stand By</div>
|
||||
<div className="call-to-action">Check runs incoming!</div>
|
||||
</div>
|
||||
|
@ -340,6 +340,7 @@ export class CICheckRunPopover extends React.PureComponent<
|
|||
)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
<div className="ci-check-run-list-header" tabIndex={0}>
|
||||
<div className="completeness-indicator">
|
||||
{this.renderCompletenessIndicator(
|
||||
|
|
|
@ -220,7 +220,7 @@ export class CICheckRunRerunDialog extends React.Component<
|
|||
if (this.state.loadingCheckSuites && this.props.checkRuns.length > 1) {
|
||||
return (
|
||||
<div className="loading-rerun-checks">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
<img src={BlankSlateImage} className="blankslate-image" alt="" />
|
||||
<div className="title">Please wait</div>
|
||||
<div className="call-to-action">
|
||||
Determining which checks can be re-run.
|
||||
|
|
|
@ -37,6 +37,7 @@ 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>
|
||||
|
|
|
@ -590,6 +590,7 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
|||
)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<dialog
|
||||
ref={this.onDialogRef}
|
||||
id={this.props.id}
|
||||
|
|
|
@ -64,6 +64,7 @@ export class DialogHeader extends React.Component<IDialogHeaderProps, {}> {
|
|||
// I don't know and we may want to revisit it at some point but for
|
||||
// now an anchor will have to do.
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus
|
||||
<a
|
||||
className="close"
|
||||
onClick={this.onCloseButtonClick}
|
||||
|
|
|
@ -34,6 +34,7 @@ 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}
|
||||
|
|
|
@ -20,7 +20,12 @@ export class ImageContainer extends React.Component<IImageProps, {}> {
|
|||
|
||||
return (
|
||||
<div className="image-wrapper">
|
||||
<img src={imageSource} style={this.props.style} onLoad={this.onLoad} />
|
||||
<img
|
||||
src={imageSource}
|
||||
style={this.props.style}
|
||||
onLoad={this.onLoad}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
ITextDiff,
|
||||
ILargeTextDiff,
|
||||
ImageDiffType,
|
||||
ISubmoduleDiff,
|
||||
} from '../../models/diff'
|
||||
import { Button } from '../lib/button'
|
||||
import {
|
||||
|
@ -31,6 +32,7 @@ import { TextDiff } from './text-diff'
|
|||
import { SideBySideDiff } from './side-by-side-diff'
|
||||
import { enableExperimentalDiffViewer } from '../../lib/feature-flag'
|
||||
import { IFileContents } from './syntax-highlighting'
|
||||
import { SubmoduleDiff } from './submodule-diff'
|
||||
|
||||
// image used when no diff is displayed
|
||||
const NoDiffImage = encodePathAsUrl(__dirname, 'static/ufo-alert.svg')
|
||||
|
@ -80,6 +82,9 @@ interface IDiffProps {
|
|||
*/
|
||||
readonly onOpenBinaryFile: (fullPath: string) => void
|
||||
|
||||
/** Called when the user requests to open a submodule. */
|
||||
readonly onOpenSubmodule?: (fullPath: string) => void
|
||||
|
||||
/**
|
||||
* Called when the user is viewing an image diff and requests
|
||||
* to change the diff presentation mode.
|
||||
|
@ -121,6 +126,8 @@ export class Diff extends React.Component<IDiffProps, IDiffState> {
|
|||
return this.renderText(diff)
|
||||
case DiffType.Binary:
|
||||
return this.renderBinaryFile()
|
||||
case DiffType.Submodule:
|
||||
return this.renderSubmoduleDiff(diff)
|
||||
case DiffType.Image:
|
||||
return this.renderImage(diff)
|
||||
case DiffType.LargeText: {
|
||||
|
@ -168,7 +175,7 @@ export class Diff extends React.Component<IDiffProps, IDiffState> {
|
|||
private renderLargeTextDiff() {
|
||||
return (
|
||||
<div className="panel empty large-diff">
|
||||
<img src={NoDiffImage} className="blankslate-image" />
|
||||
<img src={NoDiffImage} className="blankslate-image" alt="" />
|
||||
<p>
|
||||
The diff is too large to be displayed by default.
|
||||
<br />
|
||||
|
@ -185,7 +192,7 @@ export class Diff extends React.Component<IDiffProps, IDiffState> {
|
|||
private renderUnrenderableDiff() {
|
||||
return (
|
||||
<div className="panel empty large-diff">
|
||||
<img src={NoDiffImage} />
|
||||
<img src={NoDiffImage} alt="" />
|
||||
<p>The diff is too large to be displayed.</p>
|
||||
</div>
|
||||
)
|
||||
|
@ -243,6 +250,16 @@ export class Diff extends React.Component<IDiffProps, IDiffState> {
|
|||
return this.renderTextDiff(diff)
|
||||
}
|
||||
|
||||
private renderSubmoduleDiff(diff: ISubmoduleDiff) {
|
||||
return (
|
||||
<SubmoduleDiff
|
||||
onOpenSubmodule={this.props.onOpenSubmodule}
|
||||
diff={diff}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderBinaryFile() {
|
||||
return (
|
||||
<BinaryFile
|
||||
|
|
|
@ -65,6 +65,9 @@ interface ISeamlessDiffSwitcherProps {
|
|||
*/
|
||||
readonly onOpenBinaryFile: (fullPath: string) => void
|
||||
|
||||
/** Called when the user requests to open a submodule. */
|
||||
readonly onOpenSubmodule?: (fullPath: string) => void
|
||||
|
||||
/**
|
||||
* Called when the user is viewing an image diff and requests
|
||||
* to change the diff presentation mode.
|
||||
|
@ -309,6 +312,7 @@ export class SeamlessDiffSwitcher extends React.Component<
|
|||
onDiscardChanges,
|
||||
file,
|
||||
onOpenBinaryFile,
|
||||
onOpenSubmodule,
|
||||
onChangeImageDiffType,
|
||||
onHideWhitespaceInDiffChanged,
|
||||
} = this.state.propSnapshot
|
||||
|
@ -343,6 +347,7 @@ export class SeamlessDiffSwitcher extends React.Component<
|
|||
onIncludeChanged={isLoadingDiff ? noop : onIncludeChanged}
|
||||
onDiscardChanges={isLoadingDiff ? noop : onDiscardChanges}
|
||||
onOpenBinaryFile={isLoadingDiff ? noop : onOpenBinaryFile}
|
||||
onOpenSubmodule={isLoadingDiff ? noop : onOpenSubmodule}
|
||||
onChangeImageDiffType={isLoadingDiff ? noop : onChangeImageDiffType}
|
||||
onHideWhitespaceInDiffChanged={
|
||||
isLoadingDiff ? noop : onHideWhitespaceInDiffChanged
|
||||
|
|
|
@ -385,6 +385,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className="hunk-expansion-handle selectable hoverable"
|
||||
onClick={elementInfo.handler}
|
||||
|
@ -426,6 +427,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className="hunk-handle hoverable"
|
||||
onMouseEnter={this.onMouseEnterHunk}
|
||||
|
@ -462,6 +464,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={classNames('line-number', 'selectable', 'hoverable', {
|
||||
'line-selected': isSelected,
|
||||
|
|
|
@ -26,6 +26,8 @@ import {
|
|||
CellMeasurerCache,
|
||||
CellMeasurer,
|
||||
ListRowProps,
|
||||
OverscanIndicesGetterParams,
|
||||
defaultOverscanIndicesGetter,
|
||||
} from 'react-virtualized'
|
||||
import { SideBySideDiffRow } from './side-by-side-diff-row'
|
||||
import memoize from 'memoize-one'
|
||||
|
@ -75,6 +77,21 @@ export interface ISelection {
|
|||
|
||||
type ModifiedLine = { line: DiffLine; diffLineNumber: number }
|
||||
|
||||
const isElement = (n: Node): n is Element => n.nodeType === Node.ELEMENT_NODE
|
||||
const closestElement = (n: Node): Element | null =>
|
||||
isElement(n) ? n : n.parentElement
|
||||
|
||||
const closestRow = (n: Node, container: Element) => {
|
||||
const row = closestElement(n)?.closest('div[role=row]')
|
||||
if (row && container.contains(row)) {
|
||||
const rowIndex =
|
||||
row.ariaRowIndex !== null ? parseInt(row.ariaRowIndex, 10) : NaN
|
||||
return isNaN(rowIndex) ? undefined : rowIndex
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
interface ISideBySideDiffProps {
|
||||
readonly repository: Repository
|
||||
|
||||
|
@ -142,7 +159,7 @@ interface ISideBySideDiffState {
|
|||
* column is doing it. This allows us to limit text selection to that
|
||||
* specific column via CSS.
|
||||
*/
|
||||
readonly selectingTextInRow?: 'before' | 'after'
|
||||
readonly selectingTextInRow: 'before' | 'after'
|
||||
|
||||
/**
|
||||
* The current diff selection. This is used while
|
||||
|
@ -194,10 +211,14 @@ export class SideBySideDiff extends React.Component<
|
|||
ISideBySideDiffState
|
||||
> {
|
||||
private virtualListRef = React.createRef<List>()
|
||||
private diffContainer: HTMLDivElement | null = null
|
||||
|
||||
/** Diff to restore when "Collapse all expanded lines" option is used */
|
||||
private diffToRestore: ITextDiff | null = null
|
||||
|
||||
private textSelectionStartRow: number | undefined = undefined
|
||||
private textSelectionEndRow: number | undefined = undefined
|
||||
|
||||
public constructor(props: ISideBySideDiffProps) {
|
||||
super(props)
|
||||
|
||||
|
@ -205,6 +226,7 @@ export class SideBySideDiff extends React.Component<
|
|||
diff: props.diff,
|
||||
isSearching: false,
|
||||
selectedSearchResult: undefined,
|
||||
selectingTextInRow: 'before',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,12 +238,130 @@ export class SideBySideDiff extends React.Component<
|
|||
// Listen for the custom event find-text (see app.tsx)
|
||||
// and trigger the search plugin if we see it.
|
||||
document.addEventListener('find-text', this.showSearch)
|
||||
|
||||
document.addEventListener('cut', this.onCutOrCopy)
|
||||
document.addEventListener('copy', this.onCutOrCopy)
|
||||
|
||||
document.addEventListener('selectionchange', this.onDocumentSelectionChange)
|
||||
}
|
||||
|
||||
private onCutOrCopy = (ev: ClipboardEvent) => {
|
||||
if (ev.defaultPrevented || !this.isEntireDiffSelected()) {
|
||||
return
|
||||
}
|
||||
|
||||
const lineTypes = this.props.showSideBySideDiff
|
||||
? this.state.selectingTextInRow === 'before'
|
||||
? [DiffLineType.Delete, DiffLineType.Context]
|
||||
: [DiffLineType.Add, DiffLineType.Context]
|
||||
: [DiffLineType.Add, DiffLineType.Delete, DiffLineType.Context]
|
||||
|
||||
const contents = this.props.diff.hunks
|
||||
.flatMap(h =>
|
||||
h.lines
|
||||
.filter(line => lineTypes.includes(line.type))
|
||||
.map(line => line.content)
|
||||
)
|
||||
.join('\n')
|
||||
|
||||
ev.preventDefault()
|
||||
ev.clipboardData?.setData('text/plain', contents)
|
||||
}
|
||||
|
||||
private onDocumentSelectionChange = (ev: Event) => {
|
||||
if (!this.diffContainer) {
|
||||
return
|
||||
}
|
||||
|
||||
const selection = document.getSelection()
|
||||
|
||||
this.textSelectionStartRow = undefined
|
||||
this.textSelectionEndRow = undefined
|
||||
|
||||
if (!selection || selection.isCollapsed) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check to see if there's at least a partial selection within the
|
||||
// diff container. If there isn't then we want to get out of here as
|
||||
// quickly as possible.
|
||||
if (!selection.containsNode(this.diffContainer, true)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isEntireDiffSelected(selection)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the range to coerce uniform direction (i.e we don't want to have to
|
||||
// care about whether the user is selecting right to left or left to right)
|
||||
const range = selection.getRangeAt(0)
|
||||
const { startContainer, endContainer } = range
|
||||
|
||||
// The (relative) happy path is when the user is currently selecting within
|
||||
// the diff. That means that the start container will very likely be a text
|
||||
// node somewhere within a row.
|
||||
let startRow = closestRow(startContainer, this.diffContainer)
|
||||
|
||||
// If we couldn't find the row by walking upwards it's likely that the user
|
||||
// has moved their selection to the container itself or beyond (i.e dragged
|
||||
// their selection all the way up to the point where they're now selecting
|
||||
// inside the commit details).
|
||||
//
|
||||
// If so we attempt to check if the first row we're currently rendering is
|
||||
// encompassed in the selection
|
||||
if (startRow === undefined) {
|
||||
const firstRow = this.diffContainer.querySelector(
|
||||
'div[role=row]:first-child'
|
||||
)
|
||||
|
||||
if (firstRow && range.intersectsNode(firstRow)) {
|
||||
startRow = closestRow(firstRow, this.diffContainer)
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have starting row there's no point in us trying to find
|
||||
// the end row.
|
||||
if (startRow === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let endRow = closestRow(endContainer, this.diffContainer)
|
||||
|
||||
if (endRow === undefined) {
|
||||
const lastRow = this.diffContainer.querySelector(
|
||||
'div[role=row]:last-child'
|
||||
)
|
||||
|
||||
if (lastRow && range.intersectsNode(lastRow)) {
|
||||
endRow = closestRow(lastRow, this.diffContainer)
|
||||
}
|
||||
}
|
||||
|
||||
this.textSelectionStartRow = startRow
|
||||
this.textSelectionEndRow = endRow
|
||||
}
|
||||
|
||||
private isEntireDiffSelected(selection = document.getSelection()) {
|
||||
const { diffContainer } = this
|
||||
const ancestor = selection?.getRangeAt(0).commonAncestorContainer
|
||||
|
||||
// This is an artefact of the selectAllChildren call in the onSelectAll
|
||||
// handler. We can get away with checking for this since we're handling
|
||||
// the select-all event coupled with the fact that we have CSS rules which
|
||||
// prevents text selection within the diff unless focus resides within the
|
||||
// diff container.
|
||||
return ancestor === diffContainer
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.onWindowKeyDown)
|
||||
document.removeEventListener('mouseup', this.onEndSelection)
|
||||
document.removeEventListener('find-text', this.showSearch)
|
||||
document.removeEventListener(
|
||||
'selectionchange',
|
||||
this.onDocumentSelectionChange
|
||||
)
|
||||
}
|
||||
|
||||
public componentDidUpdate(
|
||||
|
@ -248,6 +388,17 @@ export class SideBySideDiff extends React.Component<
|
|||
this.props.file.id !== prevProps.file.id
|
||||
) {
|
||||
this.virtualListRef.current.scrollToPosition(0)
|
||||
|
||||
// Reset selection
|
||||
this.textSelectionStartRow = undefined
|
||||
this.textSelectionEndRow = undefined
|
||||
|
||||
if (this.diffContainer) {
|
||||
const selection = document.getSelection()
|
||||
if (selection?.containsNode(this.diffContainer, true)) {
|
||||
selection.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,6 +411,15 @@ export class SideBySideDiff extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private onDiffContainerRef = (ref: HTMLDivElement | null) => {
|
||||
if (ref === null) {
|
||||
this.diffContainer?.removeEventListener('select-all', this.onSelectAll)
|
||||
} else {
|
||||
ref.addEventListener('select-all', this.onSelectAll)
|
||||
}
|
||||
this.diffContainer = ref
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { diff } = this.state
|
||||
|
||||
|
@ -277,7 +437,12 @@ export class SideBySideDiff extends React.Component<
|
|||
})
|
||||
|
||||
return (
|
||||
<div className={containerClassName} onMouseDown={this.onMouseDown}>
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={containerClassName}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{diff.hasHiddenBidiChars && <HiddenBidiCharsWarning />}
|
||||
{this.state.isSearching && (
|
||||
<DiffSearchInput
|
||||
|
@ -285,7 +450,10 @@ export class SideBySideDiff extends React.Component<
|
|||
onClose={this.onSearchCancel}
|
||||
/>
|
||||
)}
|
||||
<div className="side-by-side-diff cm-s-default">
|
||||
<div
|
||||
className="side-by-side-diff cm-s-default"
|
||||
ref={this.onDiffContainerRef}
|
||||
>
|
||||
<AutoSizer onResize={this.clearListRowsHeightCache}>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
|
@ -296,6 +464,7 @@ export class SideBySideDiff extends React.Component<
|
|||
rowHeight={this.getRowHeight}
|
||||
rowRenderer={this.renderRow}
|
||||
ref={this.virtualListRef}
|
||||
overscanIndicesGetter={this.overscanIndicesGetter}
|
||||
// The following properties are passed to the list
|
||||
// to make sure that it gets re-rendered when any of
|
||||
// them change.
|
||||
|
@ -317,6 +486,22 @@ export class SideBySideDiff extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private overscanIndicesGetter = (params: OverscanIndicesGetterParams) => {
|
||||
const [start, end] = [this.textSelectionStartRow, this.textSelectionEndRow]
|
||||
|
||||
if (start === undefined || end === undefined) {
|
||||
return defaultOverscanIndicesGetter(params)
|
||||
}
|
||||
|
||||
const startIndex = Math.min(start, params.startIndex)
|
||||
const stopIndex = Math.max(
|
||||
params.stopIndex,
|
||||
Math.min(params.cellCount - 1, end)
|
||||
)
|
||||
|
||||
return defaultOverscanIndicesGetter({ ...params, startIndex, stopIndex })
|
||||
}
|
||||
|
||||
private renderRow = ({ index, parent, style, key }: ListRowProps) => {
|
||||
const { diff } = this.state
|
||||
const rows = getDiffRows(
|
||||
|
@ -363,7 +548,7 @@ export class SideBySideDiff extends React.Component<
|
|||
parent={parent}
|
||||
rowIndex={index}
|
||||
>
|
||||
<div key={key} style={style}>
|
||||
<div key={key} style={style} role="row" aria-rowindex={index}>
|
||||
<SideBySideDiffRow
|
||||
row={rowWithTokens}
|
||||
lineNumberWidth={lineNumberWidth}
|
||||
|
@ -635,6 +820,27 @@ export class SideBySideDiff extends React.Component<
|
|||
}
|
||||
}
|
||||
|
||||
private onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const modifiers = event.altKey || event.metaKey || event.shiftKey
|
||||
|
||||
if (!__DARWIN__ && event.key === 'a' && event.ctrlKey && !modifiers) {
|
||||
this.onSelectAll(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user presses CtrlOrCmd+A while focused within the diff
|
||||
* container or when the user triggers the select-all event. Note that this
|
||||
* deals with text-selection whereas several other methods in this component
|
||||
* named similarly deals with selection within the gutter.
|
||||
*/
|
||||
private onSelectAll = (ev?: Event | React.SyntheticEvent<unknown>) => {
|
||||
if (this.diffContainer) {
|
||||
ev?.preventDefault()
|
||||
document.getSelection()?.selectAllChildren(this.diffContainer)
|
||||
}
|
||||
}
|
||||
|
||||
private onStartSelection = (
|
||||
row: number,
|
||||
column: DiffColumn,
|
||||
|
@ -738,6 +944,10 @@ export class SideBySideDiff extends React.Component<
|
|||
role: selectionLength > 0 ? 'copy' : undefined,
|
||||
enabled: selectionLength > 0,
|
||||
},
|
||||
{
|
||||
label: __DARWIN__ ? 'Select All' : 'Select all',
|
||||
action: () => this.onSelectAll(),
|
||||
},
|
||||
]
|
||||
|
||||
const expandMenuItem = this.buildExpandMenuItem()
|
||||
|
|
200
app/src/ui/diff/submodule-diff.tsx
Normal file
200
app/src/ui/diff/submodule-diff.tsx
Normal file
|
@ -0,0 +1,200 @@
|
|||
import React from 'react'
|
||||
import { parseRepositoryIdentifier } from '../../lib/remote-parsing'
|
||||
import { ISubmoduleDiff } from '../../models/diff'
|
||||
import { LinkButton } from '../lib/link-button'
|
||||
import { TooltippedCommitSHA } from '../lib/tooltipped-commit-sha'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { SuggestedAction } from '../suggested-actions'
|
||||
|
||||
type SubmoduleItemIcon =
|
||||
| {
|
||||
readonly octicon: typeof OcticonSymbol.info
|
||||
readonly className: 'info-icon'
|
||||
}
|
||||
| {
|
||||
readonly octicon: typeof OcticonSymbol.diffModified
|
||||
readonly className: 'modified-icon'
|
||||
}
|
||||
| {
|
||||
readonly octicon: typeof OcticonSymbol.diffAdded
|
||||
readonly className: 'added-icon'
|
||||
}
|
||||
| {
|
||||
readonly octicon: typeof OcticonSymbol.diffRemoved
|
||||
readonly className: 'removed-icon'
|
||||
}
|
||||
| {
|
||||
readonly octicon: typeof OcticonSymbol.fileDiff
|
||||
readonly className: 'untracked-icon'
|
||||
}
|
||||
|
||||
interface ISubmoduleDiffProps {
|
||||
readonly onOpenSubmodule?: (fullPath: string) => void
|
||||
readonly diff: ISubmoduleDiff
|
||||
|
||||
/**
|
||||
* Whether the diff is readonly, e.g., displaying a historical diff, or the
|
||||
* diff's content can be committed, e.g., displaying a change in the working
|
||||
* directory.
|
||||
*/
|
||||
readonly readOnly: boolean
|
||||
}
|
||||
|
||||
export class SubmoduleDiff extends React.Component<ISubmoduleDiffProps> {
|
||||
public constructor(props: ISubmoduleDiffProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="changes-interstitial submodule-diff">
|
||||
<div className="content">
|
||||
<div className="interstitial-header">
|
||||
<div className="text">
|
||||
<h1>Submodule changes</h1>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderSubmoduleInfo()}
|
||||
{this.renderCommitChangeInfo()}
|
||||
{this.renderSubmodulesChangesInfo()}
|
||||
{this.renderOpenSubmoduleAction()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderSubmoduleInfo() {
|
||||
if (this.props.diff.url === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const repoIdentifier = parseRepositoryIdentifier(this.props.diff.url)
|
||||
if (repoIdentifier === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hostname =
|
||||
repoIdentifier.hostname === 'github.com'
|
||||
? ''
|
||||
: ` (${repoIdentifier.hostname})`
|
||||
|
||||
return this.renderSubmoduleDiffItem(
|
||||
{ octicon: OcticonSymbol.info, className: 'info-icon' },
|
||||
<>
|
||||
This is a submodule based on the repository{' '}
|
||||
<LinkButton
|
||||
uri={`https://${repoIdentifier.hostname}/${repoIdentifier.owner}/${repoIdentifier.name}`}
|
||||
>
|
||||
{repoIdentifier.owner}/{repoIdentifier.name}
|
||||
{hostname}
|
||||
</LinkButton>
|
||||
.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private renderCommitChangeInfo() {
|
||||
const { diff, readOnly } = this.props
|
||||
const { oldSHA, newSHA } = diff
|
||||
|
||||
const verb = readOnly ? 'was' : 'has been'
|
||||
const suffix = readOnly
|
||||
? ''
|
||||
: ' This change can be committed to the parent repository.'
|
||||
|
||||
if (oldSHA !== null && newSHA !== null) {
|
||||
return this.renderSubmoduleDiffItem(
|
||||
{ octicon: OcticonSymbol.diffModified, className: 'modified-icon' },
|
||||
<>
|
||||
This submodule changed its commit from{' '}
|
||||
{this.renderTooltippedCommitSHA(oldSHA)} to{' '}
|
||||
{this.renderTooltippedCommitSHA(newSHA)}.{suffix}
|
||||
</>
|
||||
)
|
||||
} else if (oldSHA === null && newSHA !== null) {
|
||||
return this.renderSubmoduleDiffItem(
|
||||
{ octicon: OcticonSymbol.diffAdded, className: 'added-icon' },
|
||||
<>
|
||||
This submodule {verb} added pointing at commit{' '}
|
||||
{this.renderTooltippedCommitSHA(newSHA)}.{suffix}
|
||||
</>
|
||||
)
|
||||
} else if (oldSHA !== null && newSHA === null) {
|
||||
return this.renderSubmoduleDiffItem(
|
||||
{ octicon: OcticonSymbol.diffRemoved, className: 'removed-icon' },
|
||||
<>
|
||||
This submodule {verb} removed while it was pointing at commit{' '}
|
||||
{this.renderTooltippedCommitSHA(oldSHA)}.{suffix}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private renderTooltippedCommitSHA(sha: string) {
|
||||
return <TooltippedCommitSHA commit={sha} asRef={true} />
|
||||
}
|
||||
|
||||
private renderSubmodulesChangesInfo() {
|
||||
const { diff } = this.props
|
||||
|
||||
if (!diff.status.untrackedChanges && !diff.status.modifiedChanges) {
|
||||
return null
|
||||
}
|
||||
|
||||
const changes =
|
||||
diff.status.untrackedChanges && diff.status.modifiedChanges
|
||||
? 'modified and untracked'
|
||||
: diff.status.untrackedChanges
|
||||
? 'untracked'
|
||||
: 'modified'
|
||||
|
||||
return this.renderSubmoduleDiffItem(
|
||||
{ octicon: OcticonSymbol.fileDiff, className: 'untracked-icon' },
|
||||
<>
|
||||
This submodule has {changes} changes. Those changes must be committed
|
||||
inside of the submodule before they can be part of the parent
|
||||
repository.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private renderSubmoduleDiffItem(
|
||||
icon: SubmoduleItemIcon,
|
||||
content: React.ReactElement
|
||||
) {
|
||||
return (
|
||||
<div className="item">
|
||||
<Octicon symbol={icon.octicon} className={icon.className} />
|
||||
<div className="content">{content}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderOpenSubmoduleAction() {
|
||||
// If no url is found for the submodule, it means it can't be opened
|
||||
// This happens if the user is looking at an old commit which references
|
||||
// a submodule that got later deleted.
|
||||
if (this.props.diff.url === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<SuggestedAction
|
||||
title="Open this submodule on GitHub Desktop"
|
||||
description="You can open this submodule on GitHub Desktop as a normal repository to manage and commit any changes in it."
|
||||
buttonText={__DARWIN__ ? 'Open Repository' : 'Open repository'}
|
||||
type="primary"
|
||||
onClick={this.onOpenSubmoduleClick}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private onOpenSubmoduleClick = () => {
|
||||
this.props.onOpenSubmodule?.(this.props.diff.fullPath)
|
||||
}
|
||||
}
|
|
@ -1955,6 +1955,23 @@ export class Dispatcher {
|
|||
})
|
||||
}
|
||||
|
||||
public async openOrAddRepository(path: string): Promise<Repository | null> {
|
||||
const state = this.appStore.getState()
|
||||
const repositories = state.repositories
|
||||
const existingRepository = repositories.find(r => r.path === path)
|
||||
|
||||
if (existingRepository) {
|
||||
return await this.selectRepository(existingRepository)
|
||||
}
|
||||
|
||||
return this.appStore._startOpenInDesktop(() => {
|
||||
this.showPopup({
|
||||
type: PopupType.AddRepository,
|
||||
path,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the CLI tool.
|
||||
*
|
||||
|
|
|
@ -158,6 +158,7 @@ export class DropdownSelectButton extends React.Component<
|
|||
>
|
||||
<ul>
|
||||
{options.map(o => (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
||||
<li key={o.value} onClick={this.onSelectionChange(o)}>
|
||||
{this.renderSelectedIcon(o)}
|
||||
<div className="option-title">{o.label}</div>
|
||||
|
|
|
@ -61,6 +61,7 @@ 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}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import * as React from 'react'
|
||||
import { Commit, CommitOneLine } from '../../models/commit'
|
||||
import { GitHubRepository } from '../../models/github-repository'
|
||||
|
|
|
@ -15,12 +15,11 @@ import { DiffOptions } from '../diff/diff-options'
|
|||
import { RepositorySectionTab } from '../../lib/app-state'
|
||||
import { IChangesetData } from '../../lib/git'
|
||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||
import { clipboard } from 'electron'
|
||||
import { TooltipDirection } from '../lib/tooltip'
|
||||
import { AppFileStatusKind } from '../../models/status'
|
||||
import _ from 'lodash'
|
||||
import { LinkButton } from '../lib/link-button'
|
||||
import { UnreachableCommitsTab } from './unreachable-commits-dialog'
|
||||
import { TooltippedCommitSHA } from '../lib/tooltipped-commit-sha'
|
||||
|
||||
interface ICommitSummaryProps {
|
||||
readonly repository: Repository
|
||||
|
@ -246,6 +245,7 @@ export class CommitSummary extends React.Component<
|
|||
const icon = expanded ? OcticonSymbol.fold : OcticonSymbol.unfold
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<a onClick={onClick} className="expander">
|
||||
<Octicon symbol={icon} />
|
||||
{expanded ? 'Collapse' : 'Expand'}
|
||||
|
@ -342,11 +342,6 @@ export class CommitSummary extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private getShaRef = (useShortSha?: boolean) => {
|
||||
const { selectedCommits } = this.props
|
||||
return useShortSha ? selectedCommits[0].shortSha : selectedCommits[0].sha
|
||||
}
|
||||
|
||||
private onHighlightShasInDiff = () => {
|
||||
this.props.onHighlightShas(this.props.shasInDiff)
|
||||
}
|
||||
|
@ -388,6 +383,7 @@ export class CommitSummary extends React.Component<
|
|||
const commitsPluralized = excludedCommitsCount > 1 ? 'commits' : 'commit'
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
|
||||
<div
|
||||
className="commit-unreachable-info"
|
||||
onMouseOver={this.onHighlightShasNotInDiff}
|
||||
|
@ -435,15 +431,7 @@ export class CommitSummary extends React.Component<
|
|||
aria-label="SHA"
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.gitCommit} />
|
||||
<TooltippedContent
|
||||
className="sha"
|
||||
tooltip={this.renderShaTooltip()}
|
||||
tooltipClassName="sha-hint"
|
||||
interactive={true}
|
||||
direction={TooltipDirection.SOUTH}
|
||||
>
|
||||
{this.getShaRef(true)}
|
||||
</TooltippedContent>
|
||||
<TooltippedCommitSHA className="sha" commit={selectedCommits[0]} />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
@ -538,20 +526,6 @@ export class CommitSummary extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private renderShaTooltip() {
|
||||
return (
|
||||
<>
|
||||
<code>{this.getShaRef()}</code>
|
||||
<button onClick={this.onCopyShaButtonClick}>Copy</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private onCopyShaButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
clipboard.writeText(this.getShaRef())
|
||||
}
|
||||
|
||||
private renderChangedFilesDescription = () => {
|
||||
const fileCount = this.props.changesetData.files.length
|
||||
const filesPlural = fileCount === 1 ? 'file' : 'files'
|
||||
|
|
|
@ -71,6 +71,9 @@ interface ISelectedCommitsProps {
|
|||
*/
|
||||
readonly onOpenBinaryFile: (fullPath: string) => void
|
||||
|
||||
/** Called when the user requests to open a submodule. */
|
||||
readonly onOpenSubmodule: (fullPath: string) => void
|
||||
|
||||
/**
|
||||
* Called when the user is viewing an image diff and requests
|
||||
* to change the diff presentation mode.
|
||||
|
@ -161,6 +164,7 @@ export class SelectedCommits extends React.Component<
|
|||
onOpenBinaryFile={this.props.onOpenBinaryFile}
|
||||
onChangeImageDiffType={this.props.onChangeImageDiffType}
|
||||
onHideWhitespaceInDiffChanged={this.onHideWhitespaceInDiffChanged}
|
||||
onOpenSubmodule={this.props.onOpenSubmodule}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -316,7 +320,7 @@ export class SelectedCommits extends React.Component<
|
|||
return (
|
||||
<div id="multiple-commits-selected" className="blankslate">
|
||||
<div className="panel blankslate">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
<img src={BlankSlateImage} className="blankslate-image" alt="" />
|
||||
<div>
|
||||
<p>
|
||||
Unable to display diff when multiple{' '}
|
||||
|
@ -441,7 +445,7 @@ function NoCommitSelected() {
|
|||
|
||||
return (
|
||||
<div className="panel blankslate">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
<img src={BlankSlateImage} className="blankslate-image" alt="" />
|
||||
No commit selected
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ import { TabBar } from '../tab-bar'
|
|||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { Commit } from '../../models/commit'
|
||||
import { CommitList } from './commit-list'
|
||||
import { LinkButton } from '../lib/link-button'
|
||||
|
||||
export enum UnreachableCommitsTab {
|
||||
Unreachable,
|
||||
|
@ -127,7 +128,10 @@ export class UnreachableCommitsDialog extends React.Component<
|
|||
{this.state.selectedTab === UnreachableCommitsTab.Unreachable
|
||||
? 'not'
|
||||
: ''}{' '}
|
||||
in the ancestry path of the most recent commit in your selection.
|
||||
in the ancestry path of the most recent commit in your selection.{' '}
|
||||
<LinkButton uri="https://github.com/desktop/desktop/blob/development/docs/learn-more/unreachable-commits.md">
|
||||
Learn more.
|
||||
</LinkButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -104,6 +104,7 @@ export class AuthenticationForm extends React.Component<
|
|||
<TextBox
|
||||
label="Username or email address"
|
||||
disabled={disabled}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true}
|
||||
onValueChanged={this.onUsernameChange}
|
||||
/>
|
||||
|
|
|
@ -174,6 +174,7 @@ export class Draggable extends React.Component<IDraggableProps> {
|
|||
|
||||
public render() {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div className="draggable" onMouseDown={this.onMouseDown}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
|
|
@ -55,6 +55,7 @@ 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,6 +41,7 @@ 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}
|
||||
|
|
|
@ -250,6 +250,7 @@ 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"
|
||||
|
|
|
@ -100,6 +100,7 @@ export class FocusContainer extends React.Component<
|
|||
})
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={className}
|
||||
ref={this.onWrapperRef}
|
||||
|
|
|
@ -44,6 +44,7 @@ export class LinkButton extends React.Component<ILinkButtonProps, {}> {
|
|||
const { title } = this.props
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
|
||||
<a
|
||||
ref={this.anchorRef}
|
||||
className={className}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import classNames from 'classnames'
|
||||
import { Disposable } from 'event-kit'
|
||||
import * as React from 'react'
|
||||
|
|
|
@ -93,6 +93,7 @@ export class ListRow extends React.Component<IListRowProps, {}> {
|
|||
const style = { ...this.props.style, width: '100%' }
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
|
||||
<div
|
||||
id={this.props.id}
|
||||
aria-setsize={this.props.rowCount}
|
||||
|
|
|
@ -910,6 +910,7 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
const role = this.props.ariaMode === 'menu' ? 'menu' : 'listbox'
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/aria-activedescendant-has-tabindex
|
||||
<div
|
||||
ref={this.onRef}
|
||||
id={this.props.id}
|
||||
|
@ -972,6 +973,7 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
>
|
||||
<Grid
|
||||
aria-label={''}
|
||||
// eslint-disable-next-line jsx-a11y/aria-role
|
||||
role={''}
|
||||
ref={this.onGridRef}
|
||||
autoContainerWidth={true}
|
||||
|
|
|
@ -41,6 +41,7 @@ export class PathLabel extends React.Component<IPathLabelProps, {}> {
|
|||
? availableWidth / 2 - ResizeArrowPadding
|
||||
: undefined
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label {...props}>
|
||||
<PathText path={status.oldPath} availableWidth={segmentWidth} />
|
||||
<Octicon className="rename-arrow" symbol={OcticonSymbol.arrowRight} />
|
||||
|
@ -49,6 +50,7 @@ export class PathLabel extends React.Component<IPathLabelProps, {}> {
|
|||
)
|
||||
} else {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label {...props}>
|
||||
<PathText path={this.props.path} availableWidth={availableWidth} />
|
||||
</label>
|
||||
|
|
|
@ -367,6 +367,7 @@ export class SandboxedMarkdown extends React.PureComponent<
|
|||
ref={this.onFrameContainingDivRef}
|
||||
>
|
||||
<iframe
|
||||
title="sandboxed-markdown-component"
|
||||
className="sandboxed-markdown-component"
|
||||
sandbox=""
|
||||
ref={this.onFrameRef}
|
||||
|
|
|
@ -74,6 +74,7 @@ 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}
|
||||
|
|
|
@ -244,6 +244,7 @@ export class TextBox extends React.Component<ITextBoxProps, ITextBoxState> {
|
|||
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}
|
||||
|
|
68
app/src/ui/lib/tooltipped-commit-sha.tsx
Normal file
68
app/src/ui/lib/tooltipped-commit-sha.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { clipboard } from 'electron'
|
||||
import React from 'react'
|
||||
import { Commit, shortenSHA } from '../../models/commit'
|
||||
import { Ref } from './ref'
|
||||
import { TooltipDirection } from './tooltip'
|
||||
import { TooltippedContent } from './tooltipped-content'
|
||||
|
||||
interface ITooltippedCommitSHAProps {
|
||||
readonly className?: string
|
||||
/** Commit or long SHA of a commit to render in the component. */
|
||||
readonly commit: string | Commit
|
||||
|
||||
/** Whether or not render the commit as a Ref component. Default: false */
|
||||
readonly asRef?: boolean
|
||||
}
|
||||
|
||||
export class TooltippedCommitSHA extends React.Component<
|
||||
ITooltippedCommitSHAProps,
|
||||
{}
|
||||
> {
|
||||
private get shortSHA() {
|
||||
const { commit } = this.props
|
||||
return typeof commit === 'string' ? shortenSHA(commit) : commit.shortSha
|
||||
}
|
||||
|
||||
private get longSHA() {
|
||||
const { commit } = this.props
|
||||
return typeof commit === 'string' ? commit : commit.sha
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { className } = this.props
|
||||
|
||||
return (
|
||||
<TooltippedContent
|
||||
className={className}
|
||||
tooltip={this.renderSHATooltip()}
|
||||
tooltipClassName="sha-hint"
|
||||
interactive={true}
|
||||
direction={TooltipDirection.SOUTH}
|
||||
>
|
||||
{this.renderShortSHA()}
|
||||
</TooltippedContent>
|
||||
)
|
||||
}
|
||||
|
||||
private renderShortSHA() {
|
||||
return this.props.asRef === true ? (
|
||||
<Ref>{this.shortSHA}</Ref>
|
||||
) : (
|
||||
this.shortSHA
|
||||
)
|
||||
}
|
||||
|
||||
private renderSHATooltip() {
|
||||
return (
|
||||
<>
|
||||
<code>{this.longSHA}</code>
|
||||
<button onClick={this.onCopySHAButtonClick}>Copy</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private onCopySHAButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
clipboard.writeText(this.longSHA)
|
||||
}
|
||||
}
|
|
@ -78,6 +78,7 @@ 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}
|
||||
/>
|
||||
|
|
|
@ -64,10 +64,12 @@ export class SegmentedItem<T> extends React.Component<
|
|||
const className = isSelected ? 'selected' : undefined
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<li
|
||||
className={className}
|
||||
onClick={this.onClick}
|
||||
onDoubleClick={this.onDoubleClick}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
|
||||
role="radio"
|
||||
id={this.props.id}
|
||||
aria-checked={isSelected ? 'true' : 'false'}
|
||||
|
|
|
@ -194,6 +194,7 @@ export class VerticalSegmentedControl<T extends Key> extends React.Component<
|
|||
}
|
||||
|
||||
const label = this.props.label ? (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events
|
||||
<legend onClick={this.onLegendClick}>{this.props.label}</legend>
|
||||
) : undefined
|
||||
|
||||
|
|
|
@ -142,10 +142,12 @@ export class NoRepositoriesView extends React.Component<
|
|||
<img
|
||||
className="no-repositories-graphic-top"
|
||||
src={WelcomeLeftTopImageUri}
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
className="no-repositories-graphic-bottom"
|
||||
src={WelcomeLeftBottomImageUri}
|
||||
alt=""
|
||||
/>
|
||||
</UiView>
|
||||
)
|
||||
|
|
|
@ -221,7 +221,7 @@ export class PullRequestChecksFailed extends React.Component<
|
|||
private renderCheckRunStepsLoading(): JSX.Element {
|
||||
return (
|
||||
<div className="loading-check-runs">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
<img src={BlankSlateImage} className="blankslate-image" alt="" />
|
||||
<div className="title">Stand By</div>
|
||||
<div className="call-to-action">Check run steps incoming!</div>
|
||||
</div>
|
||||
|
@ -238,7 +238,7 @@ export class PullRequestChecksFailed extends React.Component<
|
|||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
<img src={PaperStackImage} className="blankslate-image" />
|
||||
<img src={PaperStackImage} className="blankslate-image" alt="" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import * as React from 'react'
|
||||
import { ApplicationTheme, ICustomTheme } from '../lib/application-theme'
|
||||
import { SketchPicker } from 'react-color'
|
||||
|
|
|
@ -172,6 +172,7 @@ export class ReleaseNotes extends React.Component<IReleaseNotesProps, {}> {
|
|||
<img
|
||||
className="release-note-graphic-left"
|
||||
src={ReleaseNoteHeaderLeftUri}
|
||||
alt=""
|
||||
/>
|
||||
<div className="title">
|
||||
<p className="version">Version {latestVersion}</p>
|
||||
|
@ -180,6 +181,7 @@ export class ReleaseNotes extends React.Component<IReleaseNotesProps, {}> {
|
|||
<img
|
||||
className="release-note-graphic-right"
|
||||
src={ReleaseNoteHeaderRightUri}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -252,7 +252,7 @@ export class RepositoriesList extends React.Component<
|
|||
private renderNoItems = () => {
|
||||
return (
|
||||
<div className="no-items no-results-found">
|
||||
<img src={BlankSlateImage} className="blankslate-image" />
|
||||
<img src={BlankSlateImage} className="blankslate-image" alt="" />
|
||||
<div className="title">Sorry, I can't find that repository</div>
|
||||
|
||||
<div className="protip">
|
||||
|
|
|
@ -358,6 +358,7 @@ export class RepositoryView extends React.Component<
|
|||
isWorkingTreeClean={isWorkingTreeClean}
|
||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||
onOpenBinaryFile={this.onOpenBinaryFile}
|
||||
onOpenSubmodule={this.onOpenSubmodule}
|
||||
onChangeImageDiffType={this.onChangeImageDiffType}
|
||||
onHideWhitespaceInDiffChanged={this.onHideWhitespaceInDiffChanged}
|
||||
/>
|
||||
|
@ -412,6 +413,7 @@ export class RepositoryView extends React.Component<
|
|||
hideWhitespaceInDiff={this.props.hideWhitespaceInHistoryDiff}
|
||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||
onOpenBinaryFile={this.onOpenBinaryFile}
|
||||
onOpenSubmodule={this.onOpenSubmodule}
|
||||
onChangeImageDiffType={this.onChangeImageDiffType}
|
||||
onDiffOptionsOpened={this.onDiffOptionsOpened}
|
||||
showDragOverlay={showDragOverlay}
|
||||
|
@ -489,6 +491,7 @@ export class RepositoryView extends React.Component<
|
|||
hideWhitespaceInDiff={this.props.hideWhitespaceInChangesDiff}
|
||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||
onOpenBinaryFile={this.onOpenBinaryFile}
|
||||
onOpenSubmodule={this.onOpenSubmodule}
|
||||
onChangeImageDiffType={this.onChangeImageDiffType}
|
||||
askForConfirmationOnDiscardChanges={
|
||||
this.props.askForConfirmationOnDiscardChanges
|
||||
|
@ -503,6 +506,10 @@ export class RepositoryView extends React.Component<
|
|||
openFile(fullPath, this.props.dispatcher)
|
||||
}
|
||||
|
||||
private onOpenSubmodule = (fullPath: string) => {
|
||||
this.props.dispatcher.openOrAddRepository(fullPath)
|
||||
}
|
||||
|
||||
private onChangeImageDiffType = (imageDiffType: ImageDiffType) => {
|
||||
this.props.dispatcher.changeImageDiffType(imageDiffType)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import * as React from 'react'
|
||||
import { clamp } from '../../lib/clamp'
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
import * as React from 'react'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import {
|
||||
|
|
|
@ -47,6 +47,9 @@ interface IStashDiffViewerProps {
|
|||
|
||||
/** Called when the user changes the hide whitespace in diffs setting. */
|
||||
readonly onHideWhitespaceInDiffChanged: (checked: boolean) => void
|
||||
|
||||
/** Called when the user requests to open a submodule. */
|
||||
readonly onOpenSubmodule: (fullPath: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,6 +78,7 @@ export class StashDiffViewer extends React.PureComponent<IStashDiffViewerProps>
|
|||
fileListWidth,
|
||||
onOpenBinaryFile,
|
||||
onChangeImageDiffType,
|
||||
onOpenSubmodule,
|
||||
} = this.props
|
||||
const files =
|
||||
stashEntry.files.kind === StashedChangesLoadStates.Loaded
|
||||
|
@ -96,6 +100,7 @@ export class StashDiffViewer extends React.PureComponent<IStashDiffViewerProps>
|
|||
onHideWhitespaceInDiffChanged={
|
||||
this.props.onHideWhitespaceInDiffChanged
|
||||
}
|
||||
onOpenSubmodule={onOpenSubmodule}
|
||||
/>
|
||||
) : null
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import * as React from 'react'
|
||||
import { DesktopFakeRepository } from '../../lib/desktop-fake-repository'
|
||||
import {
|
||||
|
@ -64,11 +66,13 @@ export class ThankYou extends React.Component<IThankYouProps, {}> {
|
|||
<img
|
||||
className="release-note-graphic-left"
|
||||
src={ReleaseNoteHeaderLeftUri}
|
||||
alt=""
|
||||
/>
|
||||
<div className="img-space"></div>
|
||||
<img
|
||||
className="release-note-graphic-right"
|
||||
src={ReleaseNoteHeaderRightUri}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="title">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import * as React from 'react'
|
||||
import { Octicon, OcticonSymbolType } from '../octicons'
|
||||
import classNames from 'classnames'
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import * as React from 'react'
|
||||
import { Octicon, OcticonSymbolType } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
|
|
|
@ -39,7 +39,11 @@ export class TutorialDone extends React.Component<ITutorialDoneProps, {}> {
|
|||
some suggestions for what to do next.
|
||||
</p>
|
||||
</div>
|
||||
<img src={ClappingHandsImage} className="image" />
|
||||
<img
|
||||
src={ClappingHandsImage}
|
||||
className="image"
|
||||
alt="Hands clapping"
|
||||
/>
|
||||
</div>
|
||||
<SuggestedActionGroup>
|
||||
<SuggestedAction
|
||||
|
|
|
@ -100,7 +100,7 @@ export class TutorialPanel extends React.Component<
|
|||
<div className="tutorial-panel-component panel">
|
||||
<div className="titleArea">
|
||||
<h3>Get started</h3>
|
||||
<img src={TutorialPanelImage} />
|
||||
<img src={TutorialPanelImage} alt="Partially checked check list" />
|
||||
</div>
|
||||
<ol>
|
||||
<TutorialStepInstructions
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ValidTutorialStep,
|
||||
|
|
|
@ -25,20 +25,23 @@ export class TutorialWelcome extends React.Component {
|
|||
</div>
|
||||
<ul className="definitions">
|
||||
<li>
|
||||
<img src={CodeImage} />
|
||||
<img src={CodeImage} alt="Html syntax icon" />
|
||||
<p>
|
||||
<strong>Git</strong> is the version control system.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<img src={TeamDiscussionImage} />
|
||||
<img
|
||||
src={TeamDiscussionImage}
|
||||
alt="People with discussion bubbles overhead"
|
||||
/>
|
||||
<p>
|
||||
<strong>GitHub</strong> is where you store your code and
|
||||
collaborate with others.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<img src={CloudServerImage} />
|
||||
<img src={CloudServerImage} alt="Server stack with cloud" />
|
||||
<p>
|
||||
<strong>GitHub Desktop</strong> helps you work with GitHub
|
||||
locally.
|
||||
|
|
|
@ -203,16 +203,21 @@ export class Welcome extends React.Component<IWelcomeProps, IWelcomeState> {
|
|||
<div className="welcome-left">
|
||||
<div className="welcome-content">
|
||||
{this.getComponentForCurrentStep()}
|
||||
<img className="welcome-graphic-top" src={WelcomeLeftTopImageUri} />
|
||||
<img
|
||||
className="welcome-graphic-top"
|
||||
src={WelcomeLeftTopImageUri}
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
className="welcome-graphic-bottom"
|
||||
src={WelcomeLeftBottomImageUri}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="welcome-right">
|
||||
<img className="welcome-graphic" src={WelcomeRightImageUri} />
|
||||
<img className="welcome-graphic" src={WelcomeRightImageUri} alt="" />
|
||||
</div>
|
||||
</UiView>
|
||||
)
|
||||
|
|
|
@ -1,34 +1,11 @@
|
|||
@import '../mixins';
|
||||
|
||||
#app-menu-foldout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.menu-pane {
|
||||
height: 100%;
|
||||
width: 240px;
|
||||
|
||||
// Custom widths for some menus since we don't have
|
||||
// auto-sizeable foldouts at the moment (or rather we
|
||||
// do but our List implementation prevents them from
|
||||
// working). See #3547 for an example.
|
||||
&.menu-pane-branch {
|
||||
width: 275px;
|
||||
}
|
||||
|
||||
&.menu-pane-edit {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
&.menu-pane-help {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
&.menu-pane-file {
|
||||
width: 205px;
|
||||
}
|
||||
|
||||
padding-bottom: var(--spacing-half);
|
||||
// Open panes (except the first one) should have a border on their
|
||||
// right hand side to create a divider between them and their parent
|
||||
// menu.
|
||||
|
@ -36,20 +13,8 @@
|
|||
border-left: var(--base-border);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
// We want list items in previous menus to behave as if they have focus
|
||||
// even though they don't, ie we want the selected+focus state
|
||||
// to be in effect for all parent selected menu items as well as
|
||||
// the current
|
||||
.list-item {
|
||||
&.selected {
|
||||
--text-color: var(--box-selected-active-text-color);
|
||||
--text-secondary-color: var(--box-selected-active-text-color);
|
||||
|
||||
color: var(--text-color);
|
||||
background-color: var(--box-selected-active-background-color);
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// No focus outline for the list itself. When the app menu is opened without
|
||||
|
@ -62,15 +27,21 @@
|
|||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
--text-color: var(--box-selected-active-text-color);
|
||||
--text-secondary-color: var(--box-selected-active-text-color);
|
||||
|
||||
color: var(--text-color);
|
||||
background-color: var(--box-selected-active-background-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
margin-left: var(--spacing-double);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
@import 'changes/changes-list';
|
||||
@import 'changes/undo-commit';
|
||||
@import 'changes/changes-view';
|
||||
@import 'changes/no-changes';
|
||||
@import 'changes/changes-interstitial';
|
||||
@import 'changes/oversized-files-warning';
|
||||
@import 'changes/commit-warning';
|
||||
@import 'changes/submodule-diff';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue