mirror of
https://github.com/desktop/desktop
synced 2024-10-05 23:59:33 +00:00
Merge branch 'development' into releases/3.1.4
This commit is contained in:
commit
b909d42a8b
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.1.1
|
||||
uses: peter-evans/create-pull-request@v4.2.3
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test')
|
||||
with:
|
||||
|
|
19
README.md
19
README.md
|
@ -4,7 +4,17 @@
|
|||
GitHub app. It is written in [TypeScript](https://www.typescriptlang.org) and
|
||||
uses [React](https://reactjs.org/).
|
||||
|
||||
![GitHub Desktop screenshot - Windows](https://cloud.githubusercontent.com/assets/359239/26094502/a1f56d02-3a5d-11e7-8799-23c7ba5e5106.png)
|
||||
<picture>
|
||||
<source
|
||||
srcset="https://user-images.githubusercontent.com/634063/202742848-63fa1488-6254-49b5-af7c-96a6b50ea8af.png"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<img
|
||||
width="1072"
|
||||
src="https://user-images.githubusercontent.com/634063/202742985-bb3b3b94-8aca-404a-8d8a-fd6a6f030672.png"
|
||||
alt="A screenshot of the GitHub Desktop application showing changes being viewed and committed with two attributed co-authors"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
## Where can I get it?
|
||||
|
||||
|
@ -41,8 +51,7 @@ The release notes for the latest beta versions are available [here](https://desk
|
|||
|
||||
There are several community-supported package managers that can be used to
|
||||
install GitHub Desktop:
|
||||
- Windows users can install using [Chocolatey](https://chocolatey.org/) package manager:
|
||||
`c:\> choco install github-desktop`
|
||||
- Windows users can install using [winget](https://docs.microsoft.com/en-us/windows/package-manager/winget/) `c:/> winget install github-desktop` or [Chocolatey](https://chocolatey.org/) `c:\> choco install github-desktop`
|
||||
- macOS users can install using [Homebrew](https://brew.sh/) package manager:
|
||||
`$ brew install --cask github`
|
||||
|
||||
|
@ -85,6 +94,10 @@ resources relevant to the project.
|
|||
|
||||
If you're looking for something to work on, check out the [help wanted](https://github.com/desktop/desktop/issues?q=is%3Aissue+is%3Aopen+label%3A%22help%20wanted%22) label.
|
||||
|
||||
## Building Desktop
|
||||
|
||||
To get your development environment set up for building Desktop, see [setup.md](./docs/contributing/setup.md).
|
||||
|
||||
## More Resources
|
||||
|
||||
See [desktop.github.com](https://desktop.github.com) for more product-oriented
|
||||
|
|
11
SECURITY.md
Normal file
11
SECURITY.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub).
|
||||
|
||||
If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways.
|
||||
|
||||
If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly using [private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).
|
||||
|
||||
If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award.
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
|
||||
|
||||
Thanks for helping make GitHub safe for everyone.
|
|
@ -148,6 +148,7 @@ const extensionModes: ReadonlyArray<IModeDefinition> = [
|
|||
'.h': 'text/x-c',
|
||||
'.cpp': 'text/x-c++src',
|
||||
'.hpp': 'text/x-c++src',
|
||||
'.ino': 'text/x-c++src',
|
||||
'.kt': 'text/x-kotlin',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1053,11 +1053,14 @@ export class API {
|
|||
public async fetchCombinedRefStatus(
|
||||
owner: string,
|
||||
name: string,
|
||||
ref: string
|
||||
ref: string,
|
||||
reloadCache: boolean = false
|
||||
): Promise<IAPIRefStatus | null> {
|
||||
const safeRef = encodeURIComponent(ref)
|
||||
const path = `repos/${owner}/${name}/commits/${safeRef}/status?per_page=100`
|
||||
const response = await this.request('GET', path)
|
||||
const response = await this.request('GET', path, {
|
||||
reloadCache,
|
||||
})
|
||||
|
||||
try {
|
||||
return await parsedResponse<IAPIRefStatus>(response)
|
||||
|
@ -1076,7 +1079,8 @@ export class API {
|
|||
public async fetchRefCheckRuns(
|
||||
owner: string,
|
||||
name: string,
|
||||
ref: string
|
||||
ref: string,
|
||||
reloadCache: boolean = false
|
||||
): Promise<IAPIRefCheckRuns | null> {
|
||||
const safeRef = encodeURIComponent(ref)
|
||||
const path = `repos/${owner}/${name}/commits/${safeRef}/check-runs?per_page=100`
|
||||
|
@ -1084,7 +1088,10 @@ export class API {
|
|||
Accept: 'application/vnd.github.antiope-preview+json',
|
||||
}
|
||||
|
||||
const response = await this.request('GET', path, { customHeaders: headers })
|
||||
const response = await this.request('GET', path, {
|
||||
customHeaders: headers,
|
||||
reloadCache,
|
||||
})
|
||||
|
||||
try {
|
||||
return await parsedResponse<IAPIRefCheckRuns>(response)
|
||||
|
|
|
@ -11,7 +11,10 @@ import { IMenu } from '../models/app-menu'
|
|||
import { IRemote } from '../models/remote'
|
||||
import { CloneRepositoryTab } from '../models/clone-repository-tab'
|
||||
import { BranchesTab } from '../models/branches-tab'
|
||||
import { PullRequest } from '../models/pull-request'
|
||||
import {
|
||||
PullRequest,
|
||||
PullRequestSuggestedNextAction,
|
||||
} from '../models/pull-request'
|
||||
import { IAuthor } from '../models/author'
|
||||
import { MergeTreeResult } from '../models/merge'
|
||||
import { ICommitMessage } from '../models/commit-message'
|
||||
|
@ -22,7 +25,6 @@ import {
|
|||
ICloneProgress,
|
||||
IMultiCommitOperationProgress,
|
||||
} from '../models/progress'
|
||||
import { Popup } from '../models/popup'
|
||||
|
||||
import { SignInState } from './stores/sign-in-store'
|
||||
|
||||
|
@ -47,6 +49,7 @@ import {
|
|||
MultiCommitOperationStep,
|
||||
} from '../models/multi-commit-operation'
|
||||
import { IChangesetData } from './git'
|
||||
import { Popup } from '../models/popup'
|
||||
|
||||
export enum SelectionType {
|
||||
Repository,
|
||||
|
@ -116,6 +119,7 @@ export interface IAppState {
|
|||
readonly showWelcomeFlow: boolean
|
||||
readonly focusCommitMessage: boolean
|
||||
readonly currentPopup: Popup | null
|
||||
readonly allPopups: ReadonlyArray<Popup>
|
||||
readonly currentFoldout: Foldout | null
|
||||
readonly currentBanner: Banner | null
|
||||
|
||||
|
@ -145,7 +149,7 @@ export interface IAppState {
|
|||
*/
|
||||
readonly appMenuState: ReadonlyArray<IMenu>
|
||||
|
||||
readonly errors: ReadonlyArray<Error>
|
||||
readonly errorCount: number
|
||||
|
||||
/** Map from the emoji shortcut (e.g., :+1:) to the image's local path. */
|
||||
readonly emoji: Map<string, string>
|
||||
|
@ -170,6 +174,9 @@ export interface IAppState {
|
|||
/** The width of the files list in the stash view */
|
||||
readonly stashedFilesWidth: IConstrainedValue
|
||||
|
||||
/** The width of the files list in the pull request files changed view */
|
||||
readonly pullRequestFilesListWidth: IConstrainedValue
|
||||
|
||||
/**
|
||||
* Used to highlight access keys throughout the app when the
|
||||
* Alt key is pressed. Only applicable on non-macOS platforms.
|
||||
|
@ -194,6 +201,9 @@ export interface IAppState {
|
|||
/** Whether we should show a confirmation dialog */
|
||||
readonly askForConfirmationOnDiscardChangesPermanently: boolean
|
||||
|
||||
/** Should the app prompt the user to confirm a discard stash */
|
||||
readonly askForConfirmationOnDiscardStash: boolean
|
||||
|
||||
/** Should the app prompt the user to confirm a force push? */
|
||||
readonly askForConfirmationOnForcePush: boolean
|
||||
|
||||
|
@ -230,6 +240,9 @@ export interface IAppState {
|
|||
/** Whether we should hide white space changes in history diff */
|
||||
readonly hideWhitespaceInHistoryDiff: boolean
|
||||
|
||||
/** Whether we should hide white space changes in the pull request diff */
|
||||
readonly hideWhitespaceInPullRequestDiff: boolean
|
||||
|
||||
/** Whether we should show side by side diffs */
|
||||
readonly showSideBySideDiff: boolean
|
||||
|
||||
|
@ -302,6 +315,11 @@ export interface IAppState {
|
|||
* Whether or not the user enabled high-signal notifications.
|
||||
*/
|
||||
readonly notificationsEnabled: boolean
|
||||
|
||||
/** The users last chosen pull request suggested next action. */
|
||||
readonly pullRequestSuggestedNextAction:
|
||||
| PullRequestSuggestedNextAction
|
||||
| undefined
|
||||
}
|
||||
|
||||
export enum FoldoutType {
|
||||
|
@ -950,7 +968,7 @@ export interface IPullRequestState {
|
|||
* The base branch of a a pull request - the branch the currently checked out
|
||||
* branch would merge into
|
||||
*/
|
||||
readonly baseBranch: Branch
|
||||
readonly baseBranch: Branch | null
|
||||
|
||||
/** The SHAs of commits of the pull request */
|
||||
readonly commitSHAs: ReadonlyArray<string> | null
|
||||
|
@ -964,5 +982,8 @@ export interface IPullRequestState {
|
|||
* repositories commit selection where the diff of all commits represents the
|
||||
* diff between the latest commit and the earliest commits parent.
|
||||
*/
|
||||
readonly commitSelection: ICommitSelection
|
||||
readonly commitSelection: ICommitSelection | null
|
||||
|
||||
/** The result of merging the pull request branch into the base branch */
|
||||
readonly mergeStatus: MergeTreeResult | null
|
||||
}
|
||||
|
|
24
app/src/lib/commit-url.ts
Normal file
24
app/src/lib/commit-url.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as crypto from 'crypto'
|
||||
import { GitHubRepository } from '../models/github-repository'
|
||||
|
||||
/** Method to create the url for viewing a commit on dotcom */
|
||||
export function createCommitURL(
|
||||
gitHubRepository: GitHubRepository,
|
||||
SHA: string,
|
||||
filePath?: string
|
||||
): string | null {
|
||||
const baseURL = gitHubRepository.htmlURL
|
||||
|
||||
if (baseURL === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (filePath === undefined) {
|
||||
return `${baseURL}/commit/${SHA}`
|
||||
}
|
||||
|
||||
const fileHash = crypto.createHash('sha256').update(filePath).digest('hex')
|
||||
const fileSuffix = '#diff-' + fileHash
|
||||
|
||||
return `${baseURL}/commit/${SHA}${fileSuffix}`
|
||||
}
|
|
@ -45,7 +45,7 @@ const editors: IDarwinExternalEditor[] = [
|
|||
},
|
||||
{
|
||||
name: 'VSCodium',
|
||||
bundleIdentifiers: ['com.visualstudio.code.oss'],
|
||||
bundleIdentifiers: ['com.visualstudio.code.oss', 'com.vscodium'],
|
||||
},
|
||||
{
|
||||
name: 'Sublime Text',
|
||||
|
@ -144,6 +144,10 @@ const editors: IDarwinExternalEditor[] = [
|
|||
name: 'Lite XL',
|
||||
bundleIdentifiers: ['com.lite-xl'],
|
||||
},
|
||||
{
|
||||
name: 'Fleet',
|
||||
bundleIdentifiers: ['Fleet.app'],
|
||||
},
|
||||
]
|
||||
|
||||
async function findApplication(
|
||||
|
|
|
@ -62,6 +62,18 @@ const editors: ILinuxExternalEditor[] = [
|
|||
name: 'Lite XL',
|
||||
paths: ['/usr/bin/lite-xl'],
|
||||
},
|
||||
{
|
||||
name: 'Jetbrains PhpStorm',
|
||||
paths: ['/snap/bin/phpstorm'],
|
||||
},
|
||||
{
|
||||
name: 'Jetbrains WebStorm',
|
||||
paths: ['/snap/bin/webstorm'],
|
||||
},
|
||||
{
|
||||
name: 'Emacs',
|
||||
paths: ['/snap/bin/emacs', '/usr/local/bin/emacs', '/usr/bin/emacs'],
|
||||
},
|
||||
]
|
||||
|
||||
async function getAvailablePath(paths: string[]): Promise<string | null> {
|
||||
|
|
|
@ -59,7 +59,17 @@ type WindowsExternalEditor = {
|
|||
readonly displayNamePrefix: string
|
||||
|
||||
/** Value of the Publisher registry key that belongs to this editor. */
|
||||
readonly publisher: string
|
||||
readonly publishers: string[]
|
||||
|
||||
/**
|
||||
* Default shell script name for JetBrains Product
|
||||
* To get the script name go to:
|
||||
* JetBrains Toolbox > Editor settings > Shell script name
|
||||
*
|
||||
* Go to `/docs/techical/editor-integration.md` for more information on
|
||||
* how to use this field.
|
||||
*/
|
||||
readonly jetBrainsToolboxScriptName?: string
|
||||
} & WindowsExternalEditorPathInfo
|
||||
|
||||
const registryKey = (key: HKEY, ...subKeys: string[]): RegistryKey => ({
|
||||
|
@ -140,21 +150,21 @@ const editors: WindowsExternalEditor[] = [
|
|||
registryKeys: [CurrentUserUninstallKey('atom')],
|
||||
executableShimPaths: [['bin', 'atom.cmd']],
|
||||
displayNamePrefix: 'Atom',
|
||||
publisher: 'GitHub Inc.',
|
||||
publishers: ['GitHub Inc.'],
|
||||
},
|
||||
{
|
||||
name: 'Atom Beta',
|
||||
registryKeys: [CurrentUserUninstallKey('atom-beta')],
|
||||
executableShimPaths: [['bin', 'atom-beta.cmd']],
|
||||
displayNamePrefix: 'Atom Beta',
|
||||
publisher: 'GitHub Inc.',
|
||||
publishers: ['GitHub Inc.'],
|
||||
},
|
||||
{
|
||||
name: 'Atom Nightly',
|
||||
registryKeys: [CurrentUserUninstallKey('atom-nightly')],
|
||||
executableShimPaths: [['bin', 'atom-nightly.cmd']],
|
||||
displayNamePrefix: 'Atom Nightly',
|
||||
publisher: 'GitHub Inc.',
|
||||
publishers: ['GitHub Inc.'],
|
||||
},
|
||||
{
|
||||
name: 'Visual Studio Code',
|
||||
|
@ -176,7 +186,7 @@ const editors: WindowsExternalEditor[] = [
|
|||
],
|
||||
executableShimPaths: [['bin', 'code.cmd']],
|
||||
displayNamePrefix: 'Microsoft Visual Studio Code',
|
||||
publisher: 'Microsoft Corporation',
|
||||
publishers: ['Microsoft Corporation'],
|
||||
},
|
||||
{
|
||||
name: 'Visual Studio Code (Insiders)',
|
||||
|
@ -198,29 +208,63 @@ const editors: WindowsExternalEditor[] = [
|
|||
],
|
||||
executableShimPaths: [['bin', 'code-insiders.cmd']],
|
||||
displayNamePrefix: 'Microsoft Visual Studio Code Insiders',
|
||||
publisher: 'Microsoft Corporation',
|
||||
publishers: ['Microsoft Corporation'],
|
||||
},
|
||||
{
|
||||
name: 'Visual Studio Codium',
|
||||
registryKeys: [
|
||||
// 64-bit version of VSCodium (user)
|
||||
CurrentUserUninstallKey('{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1'),
|
||||
// 32-bit version of VSCodium (user)
|
||||
// 32-bit version of VSCodium (user) - new key
|
||||
CurrentUserUninstallKey('{0FD05EB4-651E-4E78-A062-515204B47A3A}_is1'),
|
||||
// ARM64 version of VSCodium (user) - new key
|
||||
CurrentUserUninstallKey('{57FD70A5-1B8D-4875-9F40-C5553F094828}_is1'),
|
||||
// 64-bit version of VSCodium (system) - new key
|
||||
LocalMachineUninstallKey('{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1'),
|
||||
// 32-bit version of VSCodium (system) - new key
|
||||
Wow64LocalMachineUninstallKey(
|
||||
'{763CBF88-25C6-4B10-952F-326AE657F16B}_is1'
|
||||
),
|
||||
// ARM64 version of VSCodium (system) - new key
|
||||
LocalMachineUninstallKey('{67DEE444-3D04-4258-B92A-BC1F0FF2CAE4}_is1'),
|
||||
// 32-bit version of VSCodium (user) - old key
|
||||
CurrentUserUninstallKey('{C6065F05-9603-4FC4-8101-B9781A25D88E}}_is1'),
|
||||
// ARM64 version of VSCodium (user)
|
||||
// ARM64 version of VSCodium (user) - old key
|
||||
CurrentUserUninstallKey('{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}_is1'),
|
||||
// 64-bit version of VSCodium (system)
|
||||
// 64-bit version of VSCodium (system) - old key
|
||||
LocalMachineUninstallKey('{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}_is1'),
|
||||
// 32-bit version of VSCodium (system)
|
||||
// 32-bit version of VSCodium (system) - old key
|
||||
Wow64LocalMachineUninstallKey(
|
||||
'{E34003BB-9E10-4501-8C11-BE3FAA83F23F}_is1'
|
||||
),
|
||||
// ARM64 version of VSCodium (system)
|
||||
// ARM64 version of VSCodium (system) - old key
|
||||
LocalMachineUninstallKey('{D1ACE434-89C5-48D1-88D3-E2991DF85475}_is1'),
|
||||
],
|
||||
executableShimPaths: [['bin', 'codium.cmd']],
|
||||
displayNamePrefix: 'VSCodium',
|
||||
publisher: 'Microsoft Corporation',
|
||||
publishers: ['VSCodium', 'Microsoft Corporation'],
|
||||
},
|
||||
{
|
||||
name: 'Visual Studio Codium (Insiders)',
|
||||
registryKeys: [
|
||||
// 64-bit version of VSCodium - Insiders (user)
|
||||
CurrentUserUninstallKey('{20F79D0D-A9AC-4220-9A81-CE675FFB6B41}_is1'),
|
||||
// 32-bit version of VSCodium - Insiders (user)
|
||||
CurrentUserUninstallKey('{ED2E5618-3E7E-4888-BF3C-A6CCC84F586F}_is1'),
|
||||
// ARM64 version of VSCodium - Insiders (user)
|
||||
CurrentUserUninstallKey('{2E362F92-14EA-455A-9ABD-3E656BBBFE71}_is1'),
|
||||
// 64-bit version of VSCodium - Insiders (system)
|
||||
LocalMachineUninstallKey('{B2E0DDB2-120E-4D34-9F7E-8C688FF839A2}_is1'),
|
||||
// 32-bit version of VSCodium - Insiders (system)
|
||||
Wow64LocalMachineUninstallKey(
|
||||
'{EF35BB36-FA7E-4BB9-B7DA-D1E09F2DA9C9}_is1'
|
||||
),
|
||||
// ARM64 version of VSCodium - Insiders (system)
|
||||
LocalMachineUninstallKey('{44721278-64C6-4513-BC45-D48E07830599}_is1'),
|
||||
],
|
||||
executableShimPaths: [['bin', 'codium-insiders.cmd']],
|
||||
displayNamePrefix: 'VSCodium (Insiders)',
|
||||
publishers: ['VSCodium'],
|
||||
},
|
||||
{
|
||||
name: 'Sublime Text',
|
||||
|
@ -232,7 +276,7 @@ const editors: WindowsExternalEditor[] = [
|
|||
],
|
||||
executableShimPaths: [['subl.exe']],
|
||||
displayNamePrefix: 'Sublime Text',
|
||||
publisher: 'Sublime HQ Pty Ltd',
|
||||
publishers: ['Sublime HQ Pty Ltd'],
|
||||
},
|
||||
{
|
||||
name: 'Brackets',
|
||||
|
@ -241,7 +285,7 @@ const editors: WindowsExternalEditor[] = [
|
|||
],
|
||||
executableShimPaths: [['Brackets.exe']],
|
||||
displayNamePrefix: 'Brackets',
|
||||
publisher: 'brackets.io',
|
||||
publishers: ['brackets.io'],
|
||||
},
|
||||
{
|
||||
name: 'ColdFusion Builder',
|
||||
|
@ -253,7 +297,7 @@ const editors: WindowsExternalEditor[] = [
|
|||
],
|
||||
executableShimPaths: [['CFBuilder.exe']],
|
||||
displayNamePrefix: 'Adobe ColdFusion Builder',
|
||||
publisher: 'Adobe Systems Incorporated',
|
||||
publishers: ['Adobe Systems Incorporated'],
|
||||
},
|
||||
{
|
||||
name: 'Typora',
|
||||
|
@ -267,7 +311,7 @@ const editors: WindowsExternalEditor[] = [
|
|||
],
|
||||
executableShimPaths: [['typora.exe']],
|
||||
displayNamePrefix: 'Typora',
|
||||
publisher: 'typora.io',
|
||||
publishers: ['typora.io'],
|
||||
},
|
||||
{
|
||||
name: 'SlickEdit',
|
||||
|
@ -297,7 +341,7 @@ const editors: WindowsExternalEditor[] = [
|
|||
],
|
||||
executableShimPaths: [['win', 'vs.exe']],
|
||||
displayNamePrefix: 'SlickEdit',
|
||||
publisher: 'SlickEdit Inc.',
|
||||
publishers: ['SlickEdit Inc.'],
|
||||
},
|
||||
{
|
||||
name: 'Aptana Studio 3',
|
||||
|
@ -306,32 +350,35 @@ const editors: WindowsExternalEditor[] = [
|
|||
],
|
||||
executableShimPaths: [['AptanaStudio3.exe']],
|
||||
displayNamePrefix: 'Aptana Studio',
|
||||
publisher: 'Appcelerator',
|
||||
publishers: ['Appcelerator'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains Webstorm',
|
||||
registryKeys: registryKeysForJetBrainsIDE('WebStorm'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('webstorm'),
|
||||
jetBrainsToolboxScriptName: 'webstorm',
|
||||
displayNamePrefix: 'WebStorm',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains Phpstorm',
|
||||
registryKeys: registryKeysForJetBrainsIDE('PhpStorm'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('phpstorm'),
|
||||
jetBrainsToolboxScriptName: 'phpstorm',
|
||||
displayNamePrefix: 'PhpStorm',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'Android Studio',
|
||||
registryKeys: [LocalMachineUninstallKey('Android Studio')],
|
||||
installLocationRegistryKey: 'UninstallString',
|
||||
jetBrainsToolboxScriptName: 'studio',
|
||||
executableShimPaths: [
|
||||
['..', 'bin', `studio64.exe`],
|
||||
['..', 'bin', `studio.exe`],
|
||||
],
|
||||
displayNamePrefix: 'Android Studio',
|
||||
publisher: 'Google LLC',
|
||||
publishers: ['Google LLC'],
|
||||
},
|
||||
{
|
||||
name: 'Notepad++',
|
||||
|
@ -343,28 +390,30 @@ const editors: WindowsExternalEditor[] = [
|
|||
],
|
||||
installLocationRegistryKey: 'DisplayIcon',
|
||||
displayNamePrefix: 'Notepad++',
|
||||
publisher: 'Notepad++ Team',
|
||||
publishers: ['Notepad++ Team'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains Rider',
|
||||
registryKeys: registryKeysForJetBrainsIDE('JetBrains Rider'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('rider'),
|
||||
jetBrainsToolboxScriptName: 'rider',
|
||||
displayNamePrefix: 'JetBrains Rider',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'RStudio',
|
||||
registryKeys: [Wow64LocalMachineUninstallKey('RStudio')],
|
||||
installLocationRegistryKey: 'DisplayIcon',
|
||||
displayNamePrefix: 'RStudio',
|
||||
publisher: 'RStudio',
|
||||
publishers: ['RStudio'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains IntelliJ Idea',
|
||||
registryKeys: registryKeysForJetBrainsIDE('IntelliJ IDEA'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('idea'),
|
||||
jetBrainsToolboxScriptName: 'idea',
|
||||
displayNamePrefix: 'IntelliJ IDEA ',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains IntelliJ Idea Community Edition',
|
||||
|
@ -373,42 +422,54 @@ const editors: WindowsExternalEditor[] = [
|
|||
),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('idea'),
|
||||
displayNamePrefix: 'IntelliJ IDEA Community Edition ',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains PyCharm',
|
||||
registryKeys: registryKeysForJetBrainsIDE('PyCharm'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('pycharm'),
|
||||
jetBrainsToolboxScriptName: 'pycharm',
|
||||
displayNamePrefix: 'PyCharm ',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains PyCharm Community Edition',
|
||||
registryKeys: registryKeysForJetBrainsIDE('PyCharm Community Edition'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('pycharm'),
|
||||
displayNamePrefix: 'PyCharm Community Edition',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains CLion',
|
||||
registryKeys: registryKeysForJetBrainsIDE('CLion'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('clion'),
|
||||
jetBrainsToolboxScriptName: 'clion',
|
||||
displayNamePrefix: 'CLion ',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains RubyMine',
|
||||
registryKeys: registryKeysForJetBrainsIDE('RubyMine'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('rubymine'),
|
||||
jetBrainsToolboxScriptName: 'rubymine',
|
||||
displayNamePrefix: 'RubyMine ',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains GoLand',
|
||||
registryKeys: registryKeysForJetBrainsIDE('GoLand'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('goland'),
|
||||
jetBrainsToolboxScriptName: 'goland',
|
||||
displayNamePrefix: 'GoLand ',
|
||||
publisher: 'JetBrains s.r.o.',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains Fleet',
|
||||
registryKeys: [LocalMachineUninstallKey('Fleet')],
|
||||
jetBrainsToolboxScriptName: 'fleet',
|
||||
installLocationRegistryKey: 'DisplayIcon',
|
||||
displayNamePrefix: 'Fleet ',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -444,7 +505,7 @@ async function findApplication(editor: WindowsExternalEditor) {
|
|||
|
||||
if (
|
||||
!displayName.startsWith(editor.displayNamePrefix) ||
|
||||
publisher !== editor.publisher
|
||||
!editor.publishers.includes(publisher)
|
||||
) {
|
||||
log.debug(`Unexpected registry entries for ${editor.name}`)
|
||||
continue
|
||||
|
@ -465,6 +526,39 @@ async function findApplication(editor: WindowsExternalEditor) {
|
|||
}
|
||||
}
|
||||
|
||||
return findJetBrainsToolboxApplication(editor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find JetBrain products installed through JetBrains Toolbox
|
||||
*/
|
||||
async function findJetBrainsToolboxApplication(editor: WindowsExternalEditor) {
|
||||
if (!editor.jetBrainsToolboxScriptName) {
|
||||
return null
|
||||
}
|
||||
|
||||
const toolboxRegistryReference = [
|
||||
CurrentUserUninstallKey('toolbox'),
|
||||
Wow64LocalMachineUninstallKey('toolbox'),
|
||||
]
|
||||
|
||||
for (const { key, subKey } of toolboxRegistryReference) {
|
||||
const keys = enumerateValues(key, subKey)
|
||||
if (keys.length > 0) {
|
||||
const editorPathInToolbox = Path.join(
|
||||
getKeyOrEmpty(keys, 'UninstallString'),
|
||||
'..',
|
||||
'..',
|
||||
'scripts',
|
||||
`${editor.jetBrainsToolboxScriptName}.cmd`
|
||||
)
|
||||
const exists = await pathExists(editorPathInToolbox)
|
||||
if (exists) {
|
||||
return editorPathInToolbox
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ export function enableWSLDetection(): boolean {
|
|||
* Should we use the new diff viewer for unified diffs?
|
||||
*/
|
||||
export function enableExperimentalDiffViewer(): boolean {
|
||||
return false
|
||||
return enableBetaFeatures()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,5 +110,15 @@ export function enableSubmoduleDiff(): boolean {
|
|||
|
||||
/** Should we enable starting pull requests? */
|
||||
export function enableStartingPullRequests(): boolean {
|
||||
return enableDevelopmentFeatures()
|
||||
return enableBetaFeatures()
|
||||
}
|
||||
|
||||
/** Should we enable starting pull requests? */
|
||||
export function enableStackedPopups(): boolean {
|
||||
return enableBetaFeatures()
|
||||
}
|
||||
|
||||
/** Should we enable mechanism to prevent closing while the app is updating? */
|
||||
export function enablePreventClosingWhileUpdating(): boolean {
|
||||
return enableBetaFeatures()
|
||||
}
|
||||
|
|
|
@ -254,7 +254,7 @@ export async function getBranchMergeBaseChangedFiles(
|
|||
baseBranchName: string,
|
||||
comparisonBranchName: string,
|
||||
latestComparisonBranchCommitRef: string
|
||||
): Promise<IChangesetData> {
|
||||
): Promise<IChangesetData | null> {
|
||||
const baseArgs = [
|
||||
'diff',
|
||||
'--merge-base',
|
||||
|
@ -268,22 +268,26 @@ export async function getBranchMergeBaseChangedFiles(
|
|||
'--',
|
||||
]
|
||||
|
||||
const result = await git(
|
||||
baseArgs,
|
||||
repository.path,
|
||||
'getBranchMergeBaseChangedFiles'
|
||||
)
|
||||
|
||||
const mergeBaseCommit = await getMergeBase(
|
||||
repository,
|
||||
baseBranchName,
|
||||
comparisonBranchName
|
||||
)
|
||||
|
||||
if (mergeBaseCommit === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = await git(
|
||||
baseArgs,
|
||||
repository.path,
|
||||
'getBranchMergeBaseChangedFiles'
|
||||
)
|
||||
|
||||
return parseRawLogWithNumstat(
|
||||
result.combinedOutput,
|
||||
`${latestComparisonBranchCommitRef}`,
|
||||
mergeBaseCommit ?? NullTreeSHA
|
||||
mergeBaseCommit
|
||||
)
|
||||
}
|
||||
|
||||
|
|
2
app/src/lib/globals.d.ts
vendored
2
app/src/lib/globals.d.ts
vendored
|
@ -162,6 +162,8 @@ interface Window {
|
|||
|
||||
interface HTMLDialogElement {
|
||||
showModal: () => void
|
||||
close: (returnValue?: string | undefined) => void
|
||||
open: boolean
|
||||
}
|
||||
/**
|
||||
* Obtain the number of elements of a tuple type
|
||||
|
|
|
@ -46,6 +46,8 @@ export type RequestChannels = {
|
|||
'menu-event': (name: MenuEvent) => void
|
||||
log: (level: LogLevel, message: string) => void
|
||||
'will-quit': () => void
|
||||
'will-quit-even-if-updating': () => void
|
||||
'cancel-quitting': () => void
|
||||
'crash-ready': () => void
|
||||
'crash-quit': () => void
|
||||
'window-state-changed': (windowState: WindowState) => void
|
||||
|
@ -63,6 +65,7 @@ export type RequestChannels = {
|
|||
blur: () => void
|
||||
'update-accounts': (accounts: ReadonlyArray<EndpointToken>) => void
|
||||
'quit-and-install-updates': () => void
|
||||
'quit-app': () => void
|
||||
'minimize-window': () => void
|
||||
'maximize-window': () => void
|
||||
'unmaximize-window': () => void
|
||||
|
@ -77,6 +80,7 @@ export type RequestChannels = {
|
|||
'focus-window': () => void
|
||||
'notification-event': NotificationCallback<DesktopAliveEvent>
|
||||
'set-window-zoom-factor': (zoomFactor: number) => void
|
||||
'show-installing-update': () => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,6 +11,7 @@ import { updateMenuState as ipcUpdateMenuState } from '../ui/main-process-proxy'
|
|||
import { AppMenu, MenuItem } from '../models/app-menu'
|
||||
import { hasConflictedFiles } from './status'
|
||||
import { findContributionTargetDefaultBranch } from './branch'
|
||||
import { enableStartingPullRequests } from './feature-flag'
|
||||
|
||||
export interface IMenuItemState {
|
||||
readonly enabled?: boolean
|
||||
|
@ -135,6 +136,7 @@ const allMenuIds: ReadonlyArray<MenuIDs> = [
|
|||
'clone-repository',
|
||||
'about',
|
||||
'create-pull-request',
|
||||
...(enableStartingPullRequests() ? ['preview-pull-request' as MenuIDs] : []),
|
||||
'squash-and-merge-branch',
|
||||
]
|
||||
|
||||
|
@ -291,6 +293,13 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder {
|
|||
'create-pull-request',
|
||||
isHostedOnGitHub && !branchIsUnborn && !onDetachedHead
|
||||
)
|
||||
if (enableStartingPullRequests()) {
|
||||
menuStateBuilder.setEnabled(
|
||||
'preview-pull-request',
|
||||
!branchIsUnborn && !onDetachedHead && isHostedOnGitHub
|
||||
)
|
||||
}
|
||||
|
||||
menuStateBuilder.setEnabled(
|
||||
'push',
|
||||
!branchIsUnborn && !onDetachedHead && !networkActionInProgress
|
||||
|
@ -330,7 +339,9 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder {
|
|||
|
||||
menuStateBuilder.disable('view-repository-on-github')
|
||||
menuStateBuilder.disable('create-pull-request')
|
||||
|
||||
if (enableStartingPullRequests()) {
|
||||
menuStateBuilder.disable('preview-pull-request')
|
||||
}
|
||||
if (
|
||||
selectedState &&
|
||||
selectedState.type === SelectionType.MissingRepository
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
conflictSteps,
|
||||
MultiCommitOperationStepKind,
|
||||
} from '../models/multi-commit-operation'
|
||||
import { Popup, PopupType } from '../models/popup'
|
||||
import { TipState } from '../models/tip'
|
||||
import { IMultiCommitOperationState, IRepositoryState } from './app-state'
|
||||
|
||||
|
@ -39,12 +38,11 @@ export function getMultiCommitOperationChooseBranchStep(
|
|||
}
|
||||
|
||||
export function isConflictsFlow(
|
||||
currentPopup: Popup | null,
|
||||
isMultiCommitOperationPopupOpen: boolean,
|
||||
multiCommitOperationState: IMultiCommitOperationState | null
|
||||
): boolean {
|
||||
return (
|
||||
currentPopup !== null &&
|
||||
currentPopup.type === PopupType.MultiCommitOperation &&
|
||||
isMultiCommitOperationPopupOpen &&
|
||||
multiCommitOperationState !== null &&
|
||||
conflictSteps.includes(multiCommitOperationState.step.kind)
|
||||
)
|
||||
|
|
204
app/src/lib/popup-manager.ts
Normal file
204
app/src/lib/popup-manager.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
import { Popup, PopupType } from '../models/popup'
|
||||
import { enableStackedPopups } from './feature-flag'
|
||||
import { sendNonFatalException } from './helpers/non-fatal-exception'
|
||||
import { uuid } from './uuid'
|
||||
|
||||
/**
|
||||
* The limit of how many popups allowed in the stack. Working under the
|
||||
* assumption that a user should only be dealing with a couple of popups at a
|
||||
* time, if a user hits the limit this would indicate a problem.
|
||||
*/
|
||||
const defaultPopupStackLimit = 50
|
||||
|
||||
/**
|
||||
* The popup manager is to manage the stack of currently open popups.
|
||||
*
|
||||
* Popup Flow Notes:
|
||||
* 1. We have many types of popups. We only support opening one popup type at a
|
||||
* time with the exception of PopupType.Error. If the app is to produce
|
||||
* multiple errors, we want the user to be able to be informed of all them.
|
||||
* 2. Error popups are viewed first ahead of any other popup types. Otherwise,
|
||||
* popups ordered by last on last off.
|
||||
* 3. There are custom error handling popups that are not categorized as errors:
|
||||
* - When a error is captured in the app, we use the dispatcher method
|
||||
* 'postError` to run through all the error handlers defined in
|
||||
* `errorHandler.ts`.
|
||||
* - If a custom error handler picks the error up, it handles it in a custom
|
||||
* way. Commonly, it users the dispatcher to open a popup specific to the
|
||||
* error - likely to allow interaction with the user. This is not an error
|
||||
* popup.
|
||||
* - Otherwise, the error is captured by the `defaultErrorHandler` defined
|
||||
* in `errorHandler.ts` which simply dispatches to `presentError`. This
|
||||
* method requests ends up in the app-store to add a popup of type `Error`
|
||||
* to the stack. Then, it is rendered as a popup with the AppError
|
||||
* component.
|
||||
* - The AppError component additionally does some custom error handling for
|
||||
* cloning errors and for author errors. But, most errors are just
|
||||
* displayed as error text with a ok button.
|
||||
*/
|
||||
export class PopupManager {
|
||||
private popupStack: ReadonlyArray<Popup> = []
|
||||
|
||||
public constructor(private readonly popupLimit = defaultPopupStackLimit) {}
|
||||
|
||||
/**
|
||||
* Returns the last popup in the stack.
|
||||
*
|
||||
* The stack is sorted such that:
|
||||
* If there are error popups, it returns the last popup of type error,
|
||||
* otherwise returns the first non-error type popup.
|
||||
*/
|
||||
public get currentPopup(): Popup | null {
|
||||
return this.popupStack.at(-1) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the popups in the stack.
|
||||
*
|
||||
* The stack is sorted such that:
|
||||
* If there are error popups, they will be the last on the stack.
|
||||
*/
|
||||
public get allPopups(): ReadonlyArray<Popup> {
|
||||
return this.popupStack
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there are any popups in the stack.
|
||||
*/
|
||||
public get isAPopupOpen(): boolean {
|
||||
return this.currentPopup !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all popups in the stack of the provided type.
|
||||
**/
|
||||
public getPopupsOfType(popupType: PopupType): ReadonlyArray<Popup> {
|
||||
return this.popupStack.filter(p => p.type === popupType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there are any popups of a given type in the stack.
|
||||
*/
|
||||
public areTherePopupsOfType(popupType: PopupType): boolean {
|
||||
return this.popupStack.some(p => p.type === popupType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a popup to the stack.
|
||||
* - The popup will be given a unique id and returned.
|
||||
* - It will not add multiple popups of the same type onto the stack
|
||||
* - NB: Error types are the only duplicates allowed
|
||||
**/
|
||||
public addPopup(popupToAdd: Popup): Popup {
|
||||
if (popupToAdd.type === PopupType.Error) {
|
||||
return this.addErrorPopup(popupToAdd.error)
|
||||
}
|
||||
|
||||
const existingPopup = this.getPopupsOfType(popupToAdd.type)
|
||||
|
||||
const popup = { id: uuid(), ...popupToAdd }
|
||||
if (!enableStackedPopups()) {
|
||||
this.popupStack = [popup, ...this.getPopupsOfType(PopupType.Error)]
|
||||
return popup
|
||||
}
|
||||
|
||||
if (existingPopup.length > 0) {
|
||||
log.warn(
|
||||
`Attempted to add a popup of already existing type - ${popupToAdd.type}.`
|
||||
)
|
||||
return popupToAdd
|
||||
}
|
||||
|
||||
this.insertBeforeErrorPopups(popup)
|
||||
this.checkStackLength()
|
||||
return popup
|
||||
}
|
||||
|
||||
/** Adds a non-Error type popup before any error popups. */
|
||||
private insertBeforeErrorPopups(popup: Popup) {
|
||||
if (this.popupStack.at(-1)?.type !== PopupType.Error) {
|
||||
this.popupStack = this.popupStack.concat(popup)
|
||||
return
|
||||
}
|
||||
|
||||
const errorPopups = this.getPopupsOfType(PopupType.Error)
|
||||
const nonErrorPopups = this.popupStack.filter(
|
||||
p => p.type !== PopupType.Error
|
||||
)
|
||||
this.popupStack = [...nonErrorPopups, popup, ...errorPopups]
|
||||
}
|
||||
|
||||
/*
|
||||
* Adds an Error Popup to the stack
|
||||
* - The popup will be given a unique id.
|
||||
* - Multiple popups of a type error.
|
||||
**/
|
||||
public addErrorPopup(error: Error): Popup {
|
||||
const popup: Popup = { id: uuid(), type: PopupType.Error, error }
|
||||
this.popupStack = this.popupStack.concat(popup)
|
||||
this.checkStackLength()
|
||||
return popup
|
||||
}
|
||||
|
||||
private checkStackLength() {
|
||||
if (this.popupStack.length > this.popupLimit) {
|
||||
// Remove the oldest
|
||||
const oldest = this.popupStack[0]
|
||||
sendNonFatalException(
|
||||
'TooManyPopups',
|
||||
new Error(
|
||||
`Max number of ${this.popupLimit} popups reached while adding popup of type ${this.currentPopup?.type}. Removing last popup from the stack -> type ${oldest.type} `
|
||||
)
|
||||
)
|
||||
this.popupStack = this.popupStack.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a popup in the stack and returns it.
|
||||
* - It uses the popup id to find and update the popup.
|
||||
*/
|
||||
public updatePopup(popupToUpdate: Popup) {
|
||||
if (popupToUpdate.id === undefined) {
|
||||
log.warn(`Attempted to update a popup without an id.`)
|
||||
return
|
||||
}
|
||||
|
||||
const index = this.popupStack.findIndex(p => p.id === popupToUpdate.id)
|
||||
if (index < 0) {
|
||||
log.warn(`Attempted to update a popup not in the stack.`)
|
||||
return
|
||||
}
|
||||
|
||||
this.popupStack = [
|
||||
...this.popupStack.slice(0, index),
|
||||
popupToUpdate,
|
||||
...this.popupStack.slice(index + 1),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a popup based on it's id.
|
||||
*/
|
||||
public removePopup(popup: Popup) {
|
||||
if (popup.id === undefined) {
|
||||
log.warn(`Attempted to remove a popup without an id.`)
|
||||
return
|
||||
}
|
||||
this.popupStack = this.popupStack.filter(p => p.id !== popup.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any popup of the given type from the stack
|
||||
*/
|
||||
public removePopupByType(popupType: PopupType) {
|
||||
this.popupStack = this.popupStack.filter(p => p.type !== popupType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes popup from the stack by it's id
|
||||
*/
|
||||
public removePopupById(popupId: string) {
|
||||
this.popupStack = this.popupStack.filter(p => p.id !== popupId)
|
||||
}
|
||||
}
|
|
@ -3,6 +3,26 @@ import { IAheadBehind } from '../models/branch'
|
|||
import { TipState } from '../models/tip'
|
||||
import { clamp } from './clamp'
|
||||
|
||||
/** Represents the force-push availability state of a branch. */
|
||||
export enum ForcePushBranchState {
|
||||
/** The branch cannot be force-pushed (it hasn't diverged from its upstream) */
|
||||
NotAvailable,
|
||||
|
||||
/**
|
||||
* The branch can be force-pushed, but the user didn't do any operation that
|
||||
* we consider should be followed by a force-push, like rebasing or amending a
|
||||
* pushed commit.
|
||||
*/
|
||||
Available,
|
||||
|
||||
/**
|
||||
* The branch can be force-pushed, and the user did some operation that we
|
||||
* consider should be followed by a force-push, like rebasing or amending a
|
||||
* pushed commit.
|
||||
*/
|
||||
Recommended,
|
||||
}
|
||||
|
||||
/**
|
||||
* Format rebase percentage to ensure it's a value between 0 and 1, but to also
|
||||
* constrain it to two significant figures, avoiding the remainder that comes
|
||||
|
@ -16,17 +36,23 @@ export function formatRebaseValue(value: number) {
|
|||
* Check application state to see whether the action applied to the current
|
||||
* branch should be a force push
|
||||
*/
|
||||
export function isCurrentBranchForcePush(
|
||||
export function getCurrentBranchForcePushState(
|
||||
branchesState: IBranchesState,
|
||||
aheadBehind: IAheadBehind | null
|
||||
) {
|
||||
): ForcePushBranchState {
|
||||
if (aheadBehind === null) {
|
||||
// no tracking branch found
|
||||
return false
|
||||
return ForcePushBranchState.NotAvailable
|
||||
}
|
||||
|
||||
const { ahead, behind } = aheadBehind
|
||||
|
||||
if (behind === 0 || ahead === 0) {
|
||||
// no a diverged branch to force push
|
||||
return ForcePushBranchState.NotAvailable
|
||||
}
|
||||
|
||||
const { tip, forcePushBranches } = branchesState
|
||||
const { ahead, behind } = aheadBehind
|
||||
|
||||
let canForcePushBranch = false
|
||||
if (tip.kind === TipState.Valid) {
|
||||
|
@ -36,5 +62,7 @@ export function isCurrentBranchForcePush(
|
|||
canForcePushBranch = foundEntry === sha
|
||||
}
|
||||
|
||||
return canForcePushBranch && behind > 0 && ahead > 0
|
||||
return canForcePushBranch
|
||||
? ForcePushBranchState.Recommended
|
||||
: ForcePushBranchState.Available
|
||||
}
|
||||
|
|
|
@ -146,9 +146,16 @@ export interface IDailyMeasures {
|
|||
/** The number of times the user committed a conflicted merge outside the merge conflicts dialog */
|
||||
readonly unguidedConflictedMergeCompletionCount: number
|
||||
|
||||
/** The number of times the user is taken to the create pull request page on dotcom */
|
||||
/** The number of times the user is taken to the create pull request page on dotcom including.
|
||||
*
|
||||
* NB - This metric tracks all times including when
|
||||
* `createPullRequestFromPreviewCount` this is tracked.
|
||||
* */
|
||||
readonly createPullRequestCount: number
|
||||
|
||||
/** The number of times the user is taken to the create pull request page on dotcom from the preview dialog */
|
||||
readonly createPullRequestFromPreviewCount: number
|
||||
|
||||
/** The number of times the rebase conflicts dialog is dismissed */
|
||||
readonly rebaseConflictsDialogDismissalCount: number
|
||||
|
||||
|
@ -467,6 +474,18 @@ export interface IDailyMeasures {
|
|||
/** The number of "checks failed" notifications the user received */
|
||||
readonly checksFailedNotificationCount: number
|
||||
|
||||
/**
|
||||
* The number of "checks failed" notifications the user received for a recent
|
||||
* repository other than the selected one.
|
||||
*/
|
||||
readonly checksFailedNotificationFromRecentRepoCount: number
|
||||
|
||||
/**
|
||||
* The number of "checks failed" notifications the user received for a
|
||||
* non-recent repository other than the selected one.
|
||||
*/
|
||||
readonly checksFailedNotificationFromNonRecentRepoCount: number
|
||||
|
||||
/** The number of "checks failed" notifications the user clicked */
|
||||
readonly checksFailedNotificationClicked: number
|
||||
|
||||
|
@ -485,6 +504,18 @@ export interface IDailyMeasures {
|
|||
*/
|
||||
readonly checksFailedDialogRerunChecksCount: number
|
||||
|
||||
/**
|
||||
* The number of PR review notifications the user received for a recent
|
||||
* repository other than the selected one.
|
||||
*/
|
||||
readonly pullRequestReviewNotificationFromRecentRepoCount: number
|
||||
|
||||
/**
|
||||
* The number of PR review notifications the user received for a non-recent
|
||||
* repository other than the selected one.
|
||||
*/
|
||||
readonly pullRequestReviewNotificationFromNonRecentRepoCount: number
|
||||
|
||||
/** The number of "approved PR" notifications the user received */
|
||||
readonly pullRequestReviewApprovedNotificationCount: number
|
||||
|
||||
|
@ -541,6 +572,9 @@ export interface IDailyMeasures {
|
|||
|
||||
/** The number of times the user opens a submodule repository from its diff */
|
||||
readonly openSubmoduleFromDiffCount: number
|
||||
|
||||
/** The number of times a user has opened the preview pull request dialog */
|
||||
readonly previewedPullRequestCount: number
|
||||
}
|
||||
|
||||
export class StatsDatabase extends Dexie {
|
||||
|
|
|
@ -112,6 +112,7 @@ const DefaultDailyMeasures: IDailyMeasures = {
|
|||
guidedConflictedMergeCompletionCount: 0,
|
||||
unguidedConflictedMergeCompletionCount: 0,
|
||||
createPullRequestCount: 0,
|
||||
createPullRequestFromPreviewCount: 0,
|
||||
rebaseConflictsDialogDismissalCount: 0,
|
||||
rebaseConflictsDialogReopenedCount: 0,
|
||||
rebaseAbortedAfterConflictsCount: 0,
|
||||
|
@ -195,10 +196,14 @@ const DefaultDailyMeasures: IDailyMeasures = {
|
|||
viewsCheckJobStepOnline: 0,
|
||||
rerunsChecks: 0,
|
||||
checksFailedNotificationCount: 0,
|
||||
checksFailedNotificationFromRecentRepoCount: 0,
|
||||
checksFailedNotificationFromNonRecentRepoCount: 0,
|
||||
checksFailedNotificationClicked: 0,
|
||||
checksFailedDialogOpenCount: 0,
|
||||
checksFailedDialogSwitchToPullRequestCount: 0,
|
||||
checksFailedDialogRerunChecksCount: 0,
|
||||
pullRequestReviewNotificationFromRecentRepoCount: 0,
|
||||
pullRequestReviewNotificationFromNonRecentRepoCount: 0,
|
||||
pullRequestReviewApprovedNotificationCount: 0,
|
||||
pullRequestReviewApprovedNotificationClicked: 0,
|
||||
pullRequestReviewApprovedDialogSwitchToPullRequestCount: 0,
|
||||
|
@ -215,6 +220,7 @@ const DefaultDailyMeasures: IDailyMeasures = {
|
|||
submoduleDiffViewedFromChangesListCount: 0,
|
||||
submoduleDiffViewedFromHistoryCount: 0,
|
||||
openSubmoduleFromDiffCount: 0,
|
||||
previewedPullRequestCount: 0,
|
||||
}
|
||||
|
||||
interface IOnboardingStats {
|
||||
|
@ -1071,6 +1077,16 @@ export class StatsStore implements IStatsStore {
|
|||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the `createPullRequestFromPreviewCount` metric
|
||||
*/
|
||||
public recordCreatePullRequestFromPreview(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
createPullRequestFromPreviewCount:
|
||||
m.createPullRequestFromPreviewCount + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the `rebaseConflictsDialogDismissalCount` metric
|
||||
*/
|
||||
|
@ -1776,6 +1792,20 @@ export class StatsStore implements IStatsStore {
|
|||
}))
|
||||
}
|
||||
|
||||
public recordChecksFailedNotificationFromRecentRepo(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
checksFailedNotificationFromRecentRepoCount:
|
||||
m.checksFailedNotificationFromRecentRepoCount + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
public recordChecksFailedNotificationFromNonRecentRepo(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
checksFailedNotificationFromNonRecentRepoCount:
|
||||
m.checksFailedNotificationFromNonRecentRepoCount + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
public recordChecksFailedNotificationClicked(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
checksFailedNotificationClicked: m.checksFailedNotificationClicked + 1,
|
||||
|
@ -1845,6 +1875,20 @@ export class StatsStore implements IStatsStore {
|
|||
return `pullRequestReview${infixMap[reviewType]}${suffix}`
|
||||
}
|
||||
|
||||
public recordPullRequestReviewNotiificationFromRecentRepo(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
pullRequestReviewNotificationFromRecentRepoCount:
|
||||
m.pullRequestReviewNotificationFromRecentRepoCount + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
public recordPullRequestReviewNotiificationFromNonRecentRepo(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
pullRequestReviewNotificationFromNonRecentRepoCount:
|
||||
m.pullRequestReviewNotificationFromNonRecentRepoCount + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
// Generic method to record stats related to Pull Request review notifications.
|
||||
private recordPullRequestReviewStat(
|
||||
reviewType: ValidNotificationPullRequestReviewState,
|
||||
|
@ -1949,6 +1993,15 @@ export class StatsStore implements IStatsStore {
|
|||
log.error(`Error reporting opt ${direction}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the `previewedPullRequestCount` metric
|
||||
*/
|
||||
public recordPreviewedPullRequest(): Promise<void> {
|
||||
return this.updateDailyMeasures(m => ({
|
||||
previewedPullRequestCount: m.previewedPullRequestCount + 1,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
PullRequestCoordinator,
|
||||
RepositoriesStore,
|
||||
SignInStore,
|
||||
UpstreamRemoteName,
|
||||
} from '.'
|
||||
import { Account } from '../../models/account'
|
||||
import { AppMenu, IMenu } from '../../models/app-menu'
|
||||
|
@ -29,7 +30,11 @@ import {
|
|||
GitHubRepository,
|
||||
hasWritePermission,
|
||||
} from '../../models/github-repository'
|
||||
import { PullRequest } from '../../models/pull-request'
|
||||
import {
|
||||
defaultPullRequestSuggestedNextAction,
|
||||
PullRequest,
|
||||
PullRequestSuggestedNextAction,
|
||||
} from '../../models/pull-request'
|
||||
import {
|
||||
forkPullRequestRemoteName,
|
||||
IRemote,
|
||||
|
@ -42,6 +47,7 @@ import {
|
|||
isRepositoryWithGitHubRepository,
|
||||
RepositoryWithGitHubRepository,
|
||||
getNonForkGitHubRepository,
|
||||
isForkedRepositoryContributingToParent,
|
||||
} from '../../models/repository'
|
||||
import {
|
||||
CommittedFileChange,
|
||||
|
@ -77,6 +83,10 @@ import {
|
|||
updatePreferredAppMenuItemLabels,
|
||||
updateAccounts,
|
||||
setWindowZoomFactor,
|
||||
onShowInstallingUpdate,
|
||||
sendWillQuitEvenIfUpdatingSync,
|
||||
quitApp,
|
||||
sendCancelQuittingSync,
|
||||
} from '../../ui/main-process-proxy'
|
||||
import {
|
||||
API,
|
||||
|
@ -180,7 +190,7 @@ import {
|
|||
matchExistingRepository,
|
||||
urlMatchesRemote,
|
||||
} from '../repository-matching'
|
||||
import { isCurrentBranchForcePush } from '../rebase'
|
||||
import { ForcePushBranchState, getCurrentBranchForcePushState } from '../rebase'
|
||||
import { RetryAction, RetryActionType } from '../../models/retry-actions'
|
||||
import {
|
||||
Default as DefaultShell,
|
||||
|
@ -304,6 +314,7 @@ import { offsetFromNow } from '../offset-from'
|
|||
import { findContributionTargetDefaultBranch } from '../branch'
|
||||
import { ValidNotificationPullRequestReview } from '../valid-notification-pull-request-review'
|
||||
import { determineMergeability } from '../git/merge-tree'
|
||||
import { PopupManager } from '../popup-manager'
|
||||
|
||||
const LastSelectedRepositoryIDKey = 'last-selected-repository-id'
|
||||
|
||||
|
@ -323,15 +334,20 @@ const commitSummaryWidthConfigKey: string = 'commit-summary-width'
|
|||
const defaultStashedFilesWidth: number = 250
|
||||
const stashedFilesWidthConfigKey: string = 'stashed-files-width'
|
||||
|
||||
const defaultPullRequestFileListWidth: number = 250
|
||||
const pullRequestFileListConfigKey: string = 'pull-request-files-width'
|
||||
|
||||
const askToMoveToApplicationsFolderDefault: boolean = true
|
||||
const confirmRepoRemovalDefault: boolean = true
|
||||
const confirmDiscardChangesDefault: boolean = true
|
||||
const confirmDiscardChangesPermanentlyDefault: boolean = true
|
||||
const confirmDiscardStashDefault: boolean = true
|
||||
const askForConfirmationOnForcePushDefault = true
|
||||
const confirmUndoCommitDefault: boolean = true
|
||||
const askToMoveToApplicationsFolderKey: string = 'askToMoveToApplicationsFolder'
|
||||
const confirmRepoRemovalKey: string = 'confirmRepoRemoval'
|
||||
const confirmDiscardChangesKey: string = 'confirmDiscardChanges'
|
||||
const confirmDiscardStashKey: string = 'confirmDiscardStash'
|
||||
const confirmDiscardChangesPermanentlyKey: string =
|
||||
'confirmDiscardChangesPermanentlyKey'
|
||||
const confirmForcePushKey: string = 'confirmForcePush'
|
||||
|
@ -348,6 +364,9 @@ const hideWhitespaceInChangesDiffDefault = false
|
|||
const hideWhitespaceInChangesDiffKey = 'hide-whitespace-in-changes-diff'
|
||||
const hideWhitespaceInHistoryDiffDefault = false
|
||||
const hideWhitespaceInHistoryDiffKey = 'hide-whitespace-in-diff'
|
||||
const hideWhitespaceInPullRequestDiffDefault = false
|
||||
const hideWhitespaceInPullRequestDiffKey =
|
||||
'hide-whitespace-in-pull-request-diff'
|
||||
|
||||
const commitSpellcheckEnabledDefault = true
|
||||
const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled'
|
||||
|
@ -370,6 +389,9 @@ const MaxInvalidFoldersToDisplay = 3
|
|||
|
||||
const lastThankYouKey = 'version-and-users-of-last-thank-you'
|
||||
const customThemeKey = 'custom-theme-key'
|
||||
const pullRequestSuggestedNextActionKey =
|
||||
'pull-request-suggested-next-action-key'
|
||||
|
||||
export class AppStore extends TypedBaseStore<IAppState> {
|
||||
private readonly gitStoreCache: GitStoreCache
|
||||
|
||||
|
@ -388,10 +410,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
private showWelcomeFlow = false
|
||||
private focusCommitMessage = false
|
||||
private currentPopup: Popup | null = null
|
||||
private currentFoldout: Foldout | null = null
|
||||
private currentBanner: Banner | null = null
|
||||
private errors: ReadonlyArray<Error> = new Array<Error>()
|
||||
private emitQueued = false
|
||||
|
||||
private readonly localRepositoryStateLookup = new Map<
|
||||
|
@ -424,6 +444,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
private sidebarWidth = constrain(defaultSidebarWidth)
|
||||
private commitSummaryWidth = constrain(defaultCommitSummaryWidth)
|
||||
private stashedFilesWidth = constrain(defaultStashedFilesWidth)
|
||||
private pullRequestFileListWidth = constrain(defaultPullRequestFileListWidth)
|
||||
|
||||
private windowState: WindowState | null = null
|
||||
private windowZoomFactor: number = 1
|
||||
|
@ -437,6 +458,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
private confirmDiscardChanges: boolean = confirmDiscardChangesDefault
|
||||
private confirmDiscardChangesPermanently: boolean =
|
||||
confirmDiscardChangesPermanentlyDefault
|
||||
private confirmDiscardStash: boolean = confirmDiscardStashDefault
|
||||
private askForConfirmationOnForcePush = askForConfirmationOnForcePushDefault
|
||||
private confirmUndoCommit: boolean = confirmUndoCommitDefault
|
||||
private imageDiffType: ImageDiffType = imageDiffTypeDefault
|
||||
|
@ -444,6 +466,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
hideWhitespaceInChangesDiffDefault
|
||||
private hideWhitespaceInHistoryDiff: boolean =
|
||||
hideWhitespaceInHistoryDiffDefault
|
||||
private hideWhitespaceInPullRequestDiff: boolean =
|
||||
hideWhitespaceInPullRequestDiffDefault
|
||||
/** Whether or not the spellchecker is enabled for commit summary and description */
|
||||
private commitSpellcheckEnabled: boolean = commitSpellcheckEnabledDefault
|
||||
private showSideBySideDiff: boolean = ShowSideBySideDiffDefault
|
||||
|
@ -488,6 +512,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
private lastThankYou: ILastThankYou | undefined
|
||||
private showCIStatusPopover: boolean = false
|
||||
|
||||
/** A service for managing the stack of open popups */
|
||||
private popupManager = new PopupManager()
|
||||
|
||||
private pullRequestSuggestedNextAction:
|
||||
| PullRequestSuggestedNextAction
|
||||
| undefined = undefined
|
||||
|
||||
public constructor(
|
||||
private readonly gitHubUserStore: GitHubUserStore,
|
||||
private readonly cloningRepositoriesStore: CloningRepositoriesStore,
|
||||
|
@ -570,6 +601,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
this.notificationsStore.onPullRequestReviewSubmitNotification(
|
||||
this.onPullRequestReviewSubmitNotification
|
||||
)
|
||||
|
||||
onShowInstallingUpdate(this.onShowInstallingUpdate)
|
||||
}
|
||||
|
||||
private initializeWindowState = async () => {
|
||||
|
@ -627,7 +660,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
// If there is a currently open popup, don't do anything here. Since the
|
||||
// app can only show one popup at a time, we don't want to close the current
|
||||
// one in favor of the error we're about to show.
|
||||
if (this.currentPopup !== null) {
|
||||
if (this.popupManager.isAPopupOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -640,6 +673,12 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
})
|
||||
}
|
||||
|
||||
private onShowInstallingUpdate = () => {
|
||||
this._showPopup({
|
||||
type: PopupType.InstallingUpdate,
|
||||
})
|
||||
}
|
||||
|
||||
/** Figure out what step of the tutorial the user needs to do next */
|
||||
private async updateCurrentTutorialStep(
|
||||
repository: Repository
|
||||
|
@ -892,15 +931,17 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
appIsFocused: this.appIsFocused,
|
||||
selectedState: this.getSelectedState(),
|
||||
signInState: this.signInStore.getState(),
|
||||
currentPopup: this.currentPopup,
|
||||
currentPopup: this.popupManager.currentPopup,
|
||||
allPopups: this.popupManager.allPopups,
|
||||
currentFoldout: this.currentFoldout,
|
||||
errors: this.errors,
|
||||
errorCount: this.popupManager.getPopupsOfType(PopupType.Error).length,
|
||||
showWelcomeFlow: this.showWelcomeFlow,
|
||||
focusCommitMessage: this.focusCommitMessage,
|
||||
emoji: this.emoji,
|
||||
sidebarWidth: this.sidebarWidth,
|
||||
commitSummaryWidth: this.commitSummaryWidth,
|
||||
stashedFilesWidth: this.stashedFilesWidth,
|
||||
pullRequestFilesListWidth: this.pullRequestFileListWidth,
|
||||
appMenuState: this.appMenu ? this.appMenu.openMenus : [],
|
||||
highlightAccessKeys: this.highlightAccessKeys,
|
||||
isUpdateAvailableBannerVisible: this.isUpdateAvailableBannerVisible,
|
||||
|
@ -913,6 +954,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
askForConfirmationOnDiscardChanges: this.confirmDiscardChanges,
|
||||
askForConfirmationOnDiscardChangesPermanently:
|
||||
this.confirmDiscardChangesPermanently,
|
||||
askForConfirmationOnDiscardStash: this.confirmDiscardStash,
|
||||
askForConfirmationOnForcePush: this.askForConfirmationOnForcePush,
|
||||
askForConfirmationOnUndoCommit: this.confirmUndoCommit,
|
||||
uncommittedChangesStrategy: this.uncommittedChangesStrategy,
|
||||
|
@ -920,6 +962,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
imageDiffType: this.imageDiffType,
|
||||
hideWhitespaceInChangesDiff: this.hideWhitespaceInChangesDiff,
|
||||
hideWhitespaceInHistoryDiff: this.hideWhitespaceInHistoryDiff,
|
||||
hideWhitespaceInPullRequestDiff: this.hideWhitespaceInPullRequestDiff,
|
||||
showSideBySideDiff: this.showSideBySideDiff,
|
||||
selectedShell: this.selectedShell,
|
||||
repositoryFilterText: this.repositoryFilterText,
|
||||
|
@ -939,6 +982,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
lastThankYou: this.lastThankYou,
|
||||
showCIStatusPopover: this.showCIStatusPopover,
|
||||
notificationsEnabled: getNotificationsEnabled(),
|
||||
pullRequestSuggestedNextAction: this.pullRequestSuggestedNextAction,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1426,17 +1470,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
|
||||
if (tip.kind === TipState.Valid && aheadBehind.behind > 0) {
|
||||
const mergeTreePromise = promiseWithMinimumTimeout(
|
||||
() => determineMergeability(repository, tip.branch, action.branch),
|
||||
500
|
||||
this.currentMergeTreePromise = this.setupMergabilityPromise(
|
||||
repository,
|
||||
tip.branch,
|
||||
action.branch
|
||||
)
|
||||
.catch(err => {
|
||||
log.warn(
|
||||
`Error occurred while trying to merge ${tip.branch.name} (${tip.branch.tip.sha}) and ${action.branch.name} (${action.branch.tip.sha})`,
|
||||
err
|
||||
)
|
||||
return null
|
||||
})
|
||||
.then(mergeStatus => {
|
||||
this.repositoryStateCache.updateCompareState(repository, () => ({
|
||||
mergeStatus,
|
||||
|
@ -1444,16 +1482,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
this.emitUpdate()
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
this.currentMergeTreePromise = null
|
||||
}
|
||||
|
||||
// TODO: when we have Promise.prototype.finally available we
|
||||
// should use that here to make this intent clearer
|
||||
mergeTreePromise.then(cleanup, cleanup)
|
||||
|
||||
this.currentMergeTreePromise = mergeTreePromise
|
||||
.finally(() => {
|
||||
this.currentMergeTreePromise = null
|
||||
})
|
||||
|
||||
return this.currentMergeTreePromise
|
||||
} else {
|
||||
|
@ -1465,6 +1496,23 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
}
|
||||
|
||||
private setupMergabilityPromise(
|
||||
repository: Repository,
|
||||
baseBranch: Branch,
|
||||
compareBranch: Branch
|
||||
) {
|
||||
return promiseWithMinimumTimeout(
|
||||
() => determineMergeability(repository, baseBranch, compareBranch),
|
||||
500
|
||||
).catch(err => {
|
||||
log.warn(
|
||||
`Error occurred while trying to merge ${baseBranch.name} (${baseBranch.tip.sha}) and ${compareBranch.name} (${compareBranch.tip.sha})`,
|
||||
err
|
||||
)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public _updateCompareForm<K extends keyof ICompareFormUpdate>(
|
||||
repository: Repository,
|
||||
|
@ -1717,6 +1765,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
)
|
||||
setNumberArray(RecentRepositoriesKey, slicedRecentRepositories)
|
||||
this.recentRepositories = slicedRecentRepositories
|
||||
this.notificationsStore.setRecentRepositories(
|
||||
this.repositories.filter(r => this.recentRepositories.includes(r.id))
|
||||
)
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
|
@ -1951,8 +2002,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
this.stashedFilesWidth = constrain(
|
||||
getNumber(stashedFilesWidthConfigKey, defaultStashedFilesWidth)
|
||||
)
|
||||
this.pullRequestFileListWidth = constrain(
|
||||
getNumber(pullRequestFileListConfigKey, defaultPullRequestFileListWidth)
|
||||
)
|
||||
|
||||
this.updateResizableConstraints()
|
||||
// TODO: Initiliaze here for now... maybe move to dialog mounting
|
||||
this.updatePullRequestResizableConstraints()
|
||||
|
||||
this.askToMoveToApplicationsFolderSetting = getBoolean(
|
||||
askToMoveToApplicationsFolderKey,
|
||||
|
@ -1974,6 +2030,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
confirmDiscardChangesPermanentlyDefault
|
||||
)
|
||||
|
||||
this.confirmDiscardStash = getBoolean(
|
||||
confirmDiscardStashKey,
|
||||
confirmDiscardStashDefault
|
||||
)
|
||||
|
||||
this.askForConfirmationOnForcePush = getBoolean(
|
||||
confirmForcePushKey,
|
||||
askForConfirmationOnForcePushDefault
|
||||
|
@ -2011,6 +2072,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
hideWhitespaceInHistoryDiffKey,
|
||||
false
|
||||
)
|
||||
this.hideWhitespaceInPullRequestDiff = getBoolean(
|
||||
hideWhitespaceInPullRequestDiffKey,
|
||||
false
|
||||
)
|
||||
this.commitSpellcheckEnabled = getBoolean(
|
||||
commitSpellcheckEnabledKey,
|
||||
commitSpellcheckEnabledDefault
|
||||
|
@ -2034,6 +2099,12 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
this.lastThankYou = getObject<ILastThankYou>(lastThankYouKey)
|
||||
|
||||
this.pullRequestSuggestedNextAction =
|
||||
getEnum(
|
||||
pullRequestSuggestedNextActionKey,
|
||||
PullRequestSuggestedNextAction
|
||||
) ?? defaultPullRequestSuggestedNextAction
|
||||
|
||||
this.emitUpdateNow()
|
||||
|
||||
this.accountsStore.refresh()
|
||||
|
@ -2077,6 +2148,41 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
this.stashedFilesWidth = constrain(this.stashedFilesWidth, 100, filesMax)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the constraints of the resizable pane in the pull request dialog
|
||||
* whenever the window dimensions change.
|
||||
*/
|
||||
private updatePullRequestResizableConstraints() {
|
||||
// TODO: Get width of PR dialog -> determine if we will have default width
|
||||
// for pr dialog. The goal is for it expand to fill some percent of
|
||||
// available window so it will change on window resize. We may have some max
|
||||
// value and min value of where to derive a default is we cannot obtain the
|
||||
// width for some reason (like initialization nad no pr dialog is open)
|
||||
// Thoughts -> ß
|
||||
// 1. Use dialog id to grab dialog if exists, else use default
|
||||
// 2. Pass dialog width up when and call this contrainst on dialog mounting
|
||||
// to initialize and subscribe to window resize inside dialog to be able
|
||||
// to pass up dialog width on window resize.
|
||||
|
||||
// Get the width of the dialog
|
||||
const available = 850
|
||||
const dialogPadding = 20
|
||||
|
||||
// This is a pretty silly width for a diff but it will fit ~9 chars per line
|
||||
// in unified mode after subtracting the width of the unified gutter and ~4
|
||||
// chars per side in split diff mode. No one would want to use it this way
|
||||
// but it doesn't break the layout and it allows users to temporarily
|
||||
// maximize the width of the file list to see long path names.
|
||||
const diffPaneMinWidth = 150
|
||||
const filesListMax = available - dialogPadding - diffPaneMinWidth
|
||||
|
||||
this.pullRequestFileListWidth = constrain(
|
||||
this.pullRequestFileListWidth,
|
||||
100,
|
||||
filesListMax
|
||||
)
|
||||
}
|
||||
|
||||
private updateSelectedExternalEditor(
|
||||
selectedEditor: string | null
|
||||
): Promise<void> {
|
||||
|
@ -2163,10 +2269,12 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
?.name ?? undefined
|
||||
}
|
||||
|
||||
const isForcePushForCurrentRepository = isCurrentBranchForcePush(
|
||||
branchesState,
|
||||
aheadBehind
|
||||
)
|
||||
// From the menu, we'll offer to force-push whenever it's possible, regardless
|
||||
// of whether or not the user performed any action we know would be followed
|
||||
// by a force-push.
|
||||
const isForcePushForCurrentRepository =
|
||||
getCurrentBranchForcePushState(branchesState, aheadBehind) !==
|
||||
ForcePushBranchState.NotAvailable
|
||||
|
||||
const isStashedChangesVisible =
|
||||
changesState.selection.kind === ChangesSelectionKind.Stash
|
||||
|
@ -2440,7 +2548,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
if (
|
||||
displayingBanner ||
|
||||
isConflictsFlow(this.currentPopup, multiCommitOperationState)
|
||||
isConflictsFlow(
|
||||
this.popupManager.areTherePopupsOfType(PopupType.MultiCommitOperation),
|
||||
multiCommitOperationState
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
@ -2530,7 +2641,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
const { multiCommitOperationState } = state
|
||||
if (
|
||||
userIsStartingMultiCommitOperation(
|
||||
this.currentPopup,
|
||||
this.popupManager.currentPopup,
|
||||
multiCommitOperationState
|
||||
)
|
||||
) {
|
||||
|
@ -3412,32 +3523,45 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public async _showPopup(popup: Popup): Promise<void> {
|
||||
this._closePopup()
|
||||
|
||||
// Always close the app menu when showing a pop up. This is only
|
||||
// applicable on Windows where we draw a custom app menu.
|
||||
this._closeFoldout(FoldoutType.AppMenu)
|
||||
|
||||
this.currentPopup = popup
|
||||
this.popupManager.addPopup(popup)
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public _closePopup(popupType?: PopupType) {
|
||||
const currentPopup = this.currentPopup
|
||||
if (currentPopup == null) {
|
||||
const currentPopup = this.popupManager.currentPopup
|
||||
if (currentPopup === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (popupType !== undefined && currentPopup.type !== popupType) {
|
||||
if (popupType === undefined) {
|
||||
this.popupManager.removePopup(currentPopup)
|
||||
} else {
|
||||
if (currentPopup.type !== popupType) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentPopup.type === PopupType.CloneRepository) {
|
||||
this._completeOpenInDesktop(() => Promise.resolve(null))
|
||||
}
|
||||
|
||||
this.popupManager.removePopupByType(popupType)
|
||||
}
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public _closePopupById(popupId: string) {
|
||||
if (this.popupManager.currentPopup === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentPopup.type === PopupType.CloneRepository) {
|
||||
this._completeOpenInDesktop(() => Promise.resolve(null))
|
||||
}
|
||||
|
||||
this.currentPopup = null
|
||||
this.popupManager.removePopupById(popupId)
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
|
@ -3859,17 +3983,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public _pushError(error: Error): Promise<void> {
|
||||
const newErrors = Array.from(this.errors)
|
||||
newErrors.push(error)
|
||||
this.errors = newErrors
|
||||
this.emitUpdate()
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public _clearError(error: Error): Promise<void> {
|
||||
this.errors = this.errors.filter(e => e !== error)
|
||||
this.popupManager.addErrorPopup(error)
|
||||
this.emitUpdate()
|
||||
|
||||
return Promise.resolve()
|
||||
|
@ -5193,6 +5307,15 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public _setConfirmDiscardStashSetting(value: boolean): Promise<void> {
|
||||
this.confirmDiscardStash = value
|
||||
|
||||
setBoolean(confirmDiscardStashKey, value)
|
||||
this.emitUpdate()
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public _setConfirmForcePushSetting(value: boolean): Promise<void> {
|
||||
this.askForConfirmationOnForcePush = value
|
||||
setBoolean(confirmForcePushKey, value)
|
||||
|
@ -5279,6 +5402,19 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
}
|
||||
|
||||
public _setHideWhitespaceInPullRequestDiff(
|
||||
hideWhitespaceInDiff: boolean,
|
||||
repository: Repository,
|
||||
file: CommittedFileChange | null
|
||||
) {
|
||||
setBoolean(hideWhitespaceInPullRequestDiffKey, hideWhitespaceInDiff)
|
||||
this.hideWhitespaceInPullRequestDiff = hideWhitespaceInDiff
|
||||
|
||||
if (file !== null) {
|
||||
this._changePullRequestFileSelection(repository, file)
|
||||
}
|
||||
}
|
||||
|
||||
public _setShowSideBySideDiff(showSideBySideDiff: boolean) {
|
||||
if (showSideBySideDiff !== this.showSideBySideDiff) {
|
||||
setShowSideBySideDiff(showSideBySideDiff)
|
||||
|
@ -5825,7 +5961,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
await this._openInBrowser(url.toString())
|
||||
}
|
||||
|
||||
public async _createPullRequest(repository: Repository): Promise<void> {
|
||||
public async _createPullRequest(
|
||||
repository: Repository,
|
||||
baseBranch?: Branch
|
||||
): Promise<void> {
|
||||
const gitHubRepository = repository.gitHubRepository
|
||||
if (!gitHubRepository) {
|
||||
return
|
||||
|
@ -5838,24 +5977,28 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return
|
||||
}
|
||||
|
||||
const branch = tip.branch
|
||||
const compareBranch = tip.branch
|
||||
const aheadBehind = state.aheadBehind
|
||||
|
||||
if (aheadBehind == null) {
|
||||
this._showPopup({
|
||||
type: PopupType.PushBranchCommits,
|
||||
repository,
|
||||
branch,
|
||||
branch: compareBranch,
|
||||
})
|
||||
} else if (aheadBehind.ahead > 0) {
|
||||
this._showPopup({
|
||||
type: PopupType.PushBranchCommits,
|
||||
repository,
|
||||
branch,
|
||||
branch: compareBranch,
|
||||
unPushedCommits: aheadBehind.ahead,
|
||||
})
|
||||
} else {
|
||||
await this._openCreatePullRequestInBrowser(repository, branch)
|
||||
await this._openCreatePullRequestInBrowser(
|
||||
repository,
|
||||
compareBranch,
|
||||
baseBranch
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5950,15 +6093,38 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
public async _openCreatePullRequestInBrowser(
|
||||
repository: Repository,
|
||||
branch: Branch
|
||||
compareBranch: Branch,
|
||||
baseBranch?: Branch
|
||||
): Promise<void> {
|
||||
const gitHubRepository = repository.gitHubRepository
|
||||
if (!gitHubRepository) {
|
||||
return
|
||||
}
|
||||
|
||||
const urlEncodedBranchName = encodeURIComponent(branch.nameWithoutRemote)
|
||||
const baseURL = `${gitHubRepository.htmlURL}/pull/new/${urlEncodedBranchName}`
|
||||
const { parent, owner, name, htmlURL } = gitHubRepository
|
||||
const isForkContributingToParent =
|
||||
isForkedRepositoryContributingToParent(repository)
|
||||
|
||||
const baseForkPreface =
|
||||
isForkContributingToParent && parent !== null
|
||||
? `${parent.owner.login}:${parent.name}:`
|
||||
: ''
|
||||
const encodedBaseBranch =
|
||||
baseBranch !== undefined
|
||||
? baseForkPreface +
|
||||
encodeURIComponent(baseBranch.nameWithoutRemote) +
|
||||
'...'
|
||||
: ''
|
||||
|
||||
const compareForkPreface = isForkContributingToParent
|
||||
? `${owner.login}:${name}:`
|
||||
: ''
|
||||
|
||||
const encodedCompareBranch =
|
||||
compareForkPreface + encodeURIComponent(compareBranch.nameWithoutRemote)
|
||||
|
||||
const compareString = `${encodedBaseBranch}${encodedCompareBranch}`
|
||||
const baseURL = `${htmlURL}/pull/new/${compareString}`
|
||||
|
||||
await this._openInBrowser(baseURL)
|
||||
|
||||
|
@ -6407,13 +6573,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
path,
|
||||
(title, value, description) => {
|
||||
if (
|
||||
this.currentPopup !== null &&
|
||||
this.currentPopup.type === PopupType.CreateTutorialRepository
|
||||
this.popupManager.currentPopup?.type ===
|
||||
PopupType.CreateTutorialRepository
|
||||
) {
|
||||
this.currentPopup = {
|
||||
...this.currentPopup,
|
||||
this.popupManager.updatePopup({
|
||||
...this.popupManager.currentPopup,
|
||||
progress: { kind: 'generic', title, value, description },
|
||||
}
|
||||
})
|
||||
this.emitUpdate()
|
||||
}
|
||||
}
|
||||
|
@ -7161,33 +7327,47 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
|
||||
public async _startPullRequest(repository: Repository) {
|
||||
const { branchesState } = this.repositoryStateCache.get(repository)
|
||||
const { defaultBranch, tip } = branchesState
|
||||
const { tip, defaultBranch } =
|
||||
this.repositoryStateCache.get(repository).branchesState
|
||||
|
||||
if (defaultBranch === null || tip.kind !== TipState.Valid) {
|
||||
if (tip.kind !== TipState.Valid) {
|
||||
// Shouldn't even be able to get here if so - just a type check
|
||||
return
|
||||
}
|
||||
|
||||
const currentBranch = tip.branch
|
||||
this._initializePullRequestPreview(repository, defaultBranch, currentBranch)
|
||||
}
|
||||
|
||||
private async _initializePullRequestPreview(
|
||||
repository: Repository,
|
||||
baseBranch: Branch | null,
|
||||
currentBranch: Branch
|
||||
) {
|
||||
if (baseBranch === null) {
|
||||
this.showPullRequestPopupNoBaseBranch(repository, currentBranch)
|
||||
return
|
||||
}
|
||||
|
||||
const gitStore = this.gitStoreCache.get(repository)
|
||||
|
||||
const pullRequestCommits = await gitStore.getCommitsBetweenBranches(
|
||||
defaultBranch,
|
||||
baseBranch,
|
||||
currentBranch
|
||||
)
|
||||
|
||||
const commitSHAs = pullRequestCommits.map(c => c.sha)
|
||||
const commitsBetweenBranches = pullRequestCommits.map(c => c.sha)
|
||||
|
||||
// A user may compare two branches with no changes between them.
|
||||
const emptyChangeSet = { files: [], linesAdded: 0, linesDeleted: 0 }
|
||||
const changesetData =
|
||||
commitSHAs.length > 0
|
||||
commitsBetweenBranches.length > 0
|
||||
? await gitStore.performFailableOperation(() =>
|
||||
getBranchMergeBaseChangedFiles(
|
||||
repository,
|
||||
defaultBranch.name,
|
||||
baseBranch.name,
|
||||
currentBranch.name,
|
||||
commitSHAs[0]
|
||||
commitsBetweenBranches[0]
|
||||
)
|
||||
)
|
||||
: emptyChangeSet
|
||||
|
@ -7196,25 +7376,113 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return
|
||||
}
|
||||
|
||||
const hasMergeBase = changesetData !== null
|
||||
// We don't care how many commits exist on the unrelated history that
|
||||
// can't be merged.
|
||||
const commitSHAs = hasMergeBase ? commitsBetweenBranches : []
|
||||
|
||||
this.repositoryStateCache.initializePullRequestState(repository, {
|
||||
baseBranch: defaultBranch,
|
||||
baseBranch,
|
||||
commitSHAs,
|
||||
commitSelection: {
|
||||
shas: commitSHAs,
|
||||
shasInDiff: commitSHAs,
|
||||
isContiguous: true,
|
||||
changesetData,
|
||||
changesetData: changesetData ?? emptyChangeSet,
|
||||
file: null,
|
||||
diff: null,
|
||||
},
|
||||
mergeStatus:
|
||||
commitSHAs.length > 0 || !hasMergeBase
|
||||
? {
|
||||
kind: hasMergeBase
|
||||
? ComputedAction.Loading
|
||||
: ComputedAction.Invalid,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
|
||||
if (changesetData.files.length > 0) {
|
||||
this.emitUpdate()
|
||||
|
||||
if (commitSHAs.length > 0) {
|
||||
this.setupPRMergeTreePromise(repository, baseBranch, currentBranch)
|
||||
}
|
||||
|
||||
if (changesetData !== null && changesetData.files.length > 0) {
|
||||
await this._changePullRequestFileSelection(
|
||||
repository,
|
||||
changesetData.files[0]
|
||||
)
|
||||
}
|
||||
|
||||
this.showPullRequestPopup(repository, currentBranch, commitSHAs)
|
||||
}
|
||||
|
||||
public showPullRequestPopupNoBaseBranch(
|
||||
repository: Repository,
|
||||
currentBranch: Branch
|
||||
) {
|
||||
this.repositoryStateCache.initializePullRequestState(repository, {
|
||||
baseBranch: null,
|
||||
commitSHAs: null,
|
||||
commitSelection: null,
|
||||
mergeStatus: null,
|
||||
})
|
||||
|
||||
this.emitUpdate()
|
||||
|
||||
this.showPullRequestPopup(repository, currentBranch, [])
|
||||
}
|
||||
|
||||
public showPullRequestPopup(
|
||||
repository: Repository,
|
||||
currentBranch: Branch,
|
||||
commitSHAs: ReadonlyArray<string>
|
||||
) {
|
||||
if (this.popupManager.areTherePopupsOfType(PopupType.StartPullRequest)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.statsStore.recordPreviewedPullRequest()
|
||||
|
||||
const { branchesState, localCommitSHAs } =
|
||||
this.repositoryStateCache.get(repository)
|
||||
const { allBranches, recentBranches, defaultBranch, currentPullRequest } =
|
||||
branchesState
|
||||
const gitStore = this.gitStoreCache.get(repository)
|
||||
/* We only want branches that are also on dotcom such that, when we ask a
|
||||
* user to create a pull request, the base branch also exists on dotcom.
|
||||
*/
|
||||
const remote = isForkedRepositoryContributingToParent(repository)
|
||||
? UpstreamRemoteName
|
||||
: gitStore.defaultRemote?.name
|
||||
const prBaseBranches = allBranches.filter(
|
||||
b => b.upstreamRemoteName === remote || b.remoteName === remote
|
||||
)
|
||||
const prRecentBaseBranches = recentBranches.filter(
|
||||
b => b.upstreamRemoteName === remote || b.remoteName === remote
|
||||
)
|
||||
const { imageDiffType, selectedExternalEditor, showSideBySideDiff } =
|
||||
this.getState()
|
||||
|
||||
const nonLocalCommitSHA =
|
||||
commitSHAs.length > 0 && !localCommitSHAs.includes(commitSHAs[0])
|
||||
? commitSHAs[0]
|
||||
: null
|
||||
|
||||
this._showPopup({
|
||||
type: PopupType.StartPullRequest,
|
||||
prBaseBranches,
|
||||
prRecentBaseBranches,
|
||||
currentBranch,
|
||||
defaultBranch,
|
||||
imageDiffType,
|
||||
repository,
|
||||
externalEditorLabel: selectedExternalEditor ?? undefined,
|
||||
nonLocalCommitSHA,
|
||||
showSideBySideDiff,
|
||||
currentBranchHasPullRequest: currentPullRequest !== null,
|
||||
})
|
||||
}
|
||||
|
||||
public async _changePullRequestFileSelection(
|
||||
|
@ -7233,7 +7501,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
const currentBranch = branchesState.tip.branch
|
||||
const { baseBranch, commitSHAs } = pullRequestState
|
||||
if (commitSHAs === null) {
|
||||
if (commitSHAs === null || baseBranch === null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -7244,6 +7512,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
diff: null,
|
||||
})
|
||||
)
|
||||
|
||||
this.emitUpdate()
|
||||
|
||||
if (commitSHAs.length === 0) {
|
||||
|
@ -7261,7 +7530,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
file,
|
||||
baseBranch.name,
|
||||
currentBranch.name,
|
||||
this.hideWhitespaceInHistoryDiff,
|
||||
this.hideWhitespaceInPullRequestDiff,
|
||||
commitSHAs[0]
|
||||
)
|
||||
)) ?? null
|
||||
|
@ -7284,6 +7553,88 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
public _setPullRequestFileListWidth(width: number): Promise<void> {
|
||||
this.pullRequestFileListWidth = {
|
||||
...this.pullRequestFileListWidth,
|
||||
value: width,
|
||||
}
|
||||
setNumber(pullRequestFileListConfigKey, width)
|
||||
this.updatePullRequestResizableConstraints()
|
||||
this.emitUpdate()
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public _resetPullRequestFileListWidth(): Promise<void> {
|
||||
this.pullRequestFileListWidth = {
|
||||
...this.pullRequestFileListWidth,
|
||||
value: defaultPullRequestFileListWidth,
|
||||
}
|
||||
localStorage.removeItem(pullRequestFileListConfigKey)
|
||||
this.updatePullRequestResizableConstraints()
|
||||
this.emitUpdate()
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public _updatePullRequestBaseBranch(
|
||||
repository: Repository,
|
||||
baseBranch: Branch
|
||||
) {
|
||||
const { branchesState, pullRequestState } =
|
||||
this.repositoryStateCache.get(repository)
|
||||
const { tip } = branchesState
|
||||
|
||||
if (tip.kind !== TipState.Valid) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pullRequestState === null) {
|
||||
// This would mean the user submitted PR after requesting base branch
|
||||
// update.
|
||||
return
|
||||
}
|
||||
|
||||
this._initializePullRequestPreview(repository, baseBranch, tip.branch)
|
||||
}
|
||||
|
||||
private setupPRMergeTreePromise(
|
||||
repository: Repository,
|
||||
baseBranch: Branch,
|
||||
compareBranch: Branch
|
||||
) {
|
||||
this.setupMergabilityPromise(repository, baseBranch, compareBranch).then(
|
||||
(mergeStatus: MergeTreeResult | null) => {
|
||||
this.repositoryStateCache.updatePullRequestState(repository, () => ({
|
||||
mergeStatus,
|
||||
}))
|
||||
this.emitUpdate()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public _quitApp(evenIfUpdating: boolean) {
|
||||
if (evenIfUpdating) {
|
||||
sendWillQuitEvenIfUpdatingSync()
|
||||
}
|
||||
|
||||
quitApp()
|
||||
}
|
||||
|
||||
public _cancelQuittingApp() {
|
||||
sendCancelQuittingSync()
|
||||
}
|
||||
|
||||
public _setPullRequestSuggestedNextAction(
|
||||
value: PullRequestSuggestedNextAction
|
||||
) {
|
||||
this.pullRequestSuggestedNextAction = value
|
||||
|
||||
localStorage.setItem(pullRequestSuggestedNextActionKey, value)
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,8 +2,11 @@ import {
|
|||
Repository,
|
||||
isRepositoryWithGitHubRepository,
|
||||
RepositoryWithGitHubRepository,
|
||||
isRepositoryWithForkedGitHubRepository,
|
||||
getForkContributionTarget,
|
||||
} from '../../models/repository'
|
||||
import { PullRequest } from '../../models/pull-request'
|
||||
import { ForkContributionTarget } from '../../models/workflow-preferences'
|
||||
import { getPullRequestCommitRef, PullRequest } from '../../models/pull-request'
|
||||
import { API, APICheckConclusion } from '../api'
|
||||
import {
|
||||
createCombinedCheckFromChecks,
|
||||
|
@ -66,11 +69,13 @@ export function getNotificationsEnabled() {
|
|||
*/
|
||||
export class NotificationsStore {
|
||||
private repository: RepositoryWithGitHubRepository | null = null
|
||||
private recentRepositories: ReadonlyArray<Repository> = []
|
||||
private onChecksFailedCallback: OnChecksFailedCallback | null = null
|
||||
private onPullRequestReviewSubmitCallback: OnPullRequestReviewSubmitCallback | null =
|
||||
null
|
||||
private cachedCommits: Map<string, Commit> = new Map()
|
||||
private skipCommitShas: Set<string> = new Set()
|
||||
private skipCheckRuns: Set<number> = new Set()
|
||||
|
||||
public constructor(
|
||||
private readonly accountsStore: AccountsStore,
|
||||
|
@ -121,6 +126,15 @@ export class NotificationsStore {
|
|||
return
|
||||
}
|
||||
|
||||
if (!this.isValidRepositoryForEvent(repository, event)) {
|
||||
if (this.isRecentRepositoryEvent(event)) {
|
||||
this.statsStore.recordPullRequestReviewNotiificationFromRecentRepo()
|
||||
} else {
|
||||
this.statsStore.recordPullRequestReviewNotiificationFromNonRecentRepo()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const pullRequests = await this.pullRequestCoordinator.getAllPullRequests(
|
||||
repository
|
||||
)
|
||||
|
@ -134,16 +148,17 @@ export class NotificationsStore {
|
|||
return
|
||||
}
|
||||
|
||||
const { gitHubRepository } = repository
|
||||
const api = await this.getAPIForRepository(gitHubRepository)
|
||||
// PR reviews must be retrieved from the repository the PR belongs to
|
||||
const pullsRepository = this.getContributingRepository(repository)
|
||||
const api = await this.getAPIForRepository(pullsRepository)
|
||||
|
||||
if (api === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const review = await api.fetchPullRequestReview(
|
||||
gitHubRepository.owner.login,
|
||||
gitHubRepository.name,
|
||||
pullsRepository.owner.login,
|
||||
pullsRepository.name,
|
||||
pullRequest.pullRequestNumber.toString(),
|
||||
event.review_id
|
||||
)
|
||||
|
@ -192,6 +207,15 @@ export class NotificationsStore {
|
|||
return
|
||||
}
|
||||
|
||||
if (!this.isValidRepositoryForEvent(repository, event)) {
|
||||
if (this.isRecentRepositoryEvent(event)) {
|
||||
this.statsStore.recordChecksFailedNotificationFromRecentRepo()
|
||||
} else {
|
||||
this.statsStore.recordChecksFailedNotificationFromNonRecentRepo()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const pullRequests = await this.pullRequestCoordinator.getAllPullRequests(
|
||||
repository
|
||||
)
|
||||
|
@ -234,11 +258,29 @@ export class NotificationsStore {
|
|||
return
|
||||
}
|
||||
|
||||
const checks = await this.getChecksForRef(repository, pullRequest.head.ref)
|
||||
// Checks must be retrieved from the repository the PR belongs to
|
||||
const checksRepository = this.getContributingRepository(repository)
|
||||
|
||||
const checks = await this.getChecksForRef(
|
||||
checksRepository,
|
||||
getPullRequestCommitRef(pullRequest.pullRequestNumber)
|
||||
)
|
||||
if (checks === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure we haven't shown a notification for the check runs of this
|
||||
// check suite already.
|
||||
// If one of more jobs are re-run, the check suite will have the same ID
|
||||
// but different check runs.
|
||||
const checkSuiteCheckRunIds = checks.flatMap(check =>
|
||||
check.checkSuiteId === event.check_suite_id ? check.id : []
|
||||
)
|
||||
|
||||
if (checkSuiteCheckRunIds.every(id => this.skipCheckRuns.has(id))) {
|
||||
return
|
||||
}
|
||||
|
||||
const numberOfFailedChecks = checks.filter(
|
||||
check => check.conclusion === APICheckConclusion.Failure
|
||||
).length
|
||||
|
@ -250,6 +292,12 @@ export class NotificationsStore {
|
|||
return
|
||||
}
|
||||
|
||||
// Ignore any remaining notification for check runs that started along
|
||||
// with this one.
|
||||
for (const check of checks) {
|
||||
this.skipCheckRuns.add(check.id)
|
||||
}
|
||||
|
||||
const pluralChecks =
|
||||
numberOfFailedChecks === 1 ? 'check was' : 'checks were'
|
||||
|
||||
|
@ -283,14 +331,78 @@ export class NotificationsStore {
|
|||
this.statsStore.recordChecksFailedNotificationShown()
|
||||
}
|
||||
|
||||
private getContributingRepository(
|
||||
repository: RepositoryWithGitHubRepository
|
||||
) {
|
||||
const isForkContributingToParent =
|
||||
isRepositoryWithForkedGitHubRepository(repository) &&
|
||||
getForkContributionTarget(repository) === ForkContributionTarget.Parent
|
||||
|
||||
return isForkContributingToParent
|
||||
? repository.gitHubRepository.parent
|
||||
: repository.gitHubRepository
|
||||
}
|
||||
|
||||
private isValidRepositoryForEvent(
|
||||
repository: RepositoryWithGitHubRepository,
|
||||
event: DesktopAliveEvent
|
||||
) {
|
||||
// If it's a fork and set to contribute to the parent repository, try to
|
||||
// match the parent repository.
|
||||
if (
|
||||
isRepositoryWithForkedGitHubRepository(repository) &&
|
||||
getForkContributionTarget(repository) === ForkContributionTarget.Parent
|
||||
) {
|
||||
const parentRepository = repository.gitHubRepository.parent
|
||||
return (
|
||||
parentRepository.owner.login === event.owner &&
|
||||
parentRepository.name === event.repo
|
||||
)
|
||||
}
|
||||
|
||||
const ghRepository = repository.gitHubRepository
|
||||
return (
|
||||
ghRepository.owner.login === event.owner &&
|
||||
ghRepository.name === event.repo
|
||||
)
|
||||
}
|
||||
|
||||
private isRecentRepositoryEvent(event: DesktopAliveEvent) {
|
||||
return this.recentRepositories.some(
|
||||
r =>
|
||||
isRepositoryWithGitHubRepository(r) &&
|
||||
this.isValidRepositoryForEvent(r, event)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the store to keep track of the currently selected repository. Only
|
||||
* notifications for the currently selected repository will be shown.
|
||||
*/
|
||||
public selectRepository(repository: Repository) {
|
||||
if (repository.hash === this.repository?.hash) {
|
||||
return
|
||||
}
|
||||
|
||||
this.repository = isRepositoryWithGitHubRepository(repository)
|
||||
? repository
|
||||
: null
|
||||
this.resetCache()
|
||||
}
|
||||
|
||||
private resetCache() {
|
||||
this.cachedCommits.clear()
|
||||
this.skipCommitShas.clear()
|
||||
this.skipCheckRuns.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* For stats purposes, we need to know which are the recent repositories. This
|
||||
* will allow the notification store when a notification is related to one of
|
||||
* these repositories.
|
||||
*/
|
||||
public setRecentRepositories(repositories: ReadonlyArray<Repository>) {
|
||||
this.recentRepositories = repositories
|
||||
}
|
||||
|
||||
private async getAccountForRepository(repository: GitHubRepository) {
|
||||
|
@ -310,22 +422,20 @@ export class NotificationsStore {
|
|||
return API.fromAccount(account)
|
||||
}
|
||||
|
||||
private async getChecksForRef(
|
||||
repository: RepositoryWithGitHubRepository,
|
||||
ref: string
|
||||
) {
|
||||
const { gitHubRepository } = repository
|
||||
const { owner, name } = gitHubRepository
|
||||
private async getChecksForRef(repository: GitHubRepository, ref: string) {
|
||||
const { owner, name } = repository
|
||||
|
||||
const api = await this.getAPIForRepository(gitHubRepository)
|
||||
const api = await this.getAPIForRepository(repository)
|
||||
|
||||
if (api === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Hit these API endpoints reloading the cache to make sure we have the
|
||||
// latest data at the time the notification is received.
|
||||
const [statuses, checkRuns] = await Promise.all([
|
||||
api.fetchCombinedRefStatus(owner.login, name, ref),
|
||||
api.fetchRefCheckRuns(owner.login, name, ref),
|
||||
api.fetchCombinedRefStatus(owner.login, name, ref, true),
|
||||
api.fetchRefCheckRuns(owner.login, name, ref, true),
|
||||
])
|
||||
|
||||
const checks = new Array<IRefCheck>()
|
||||
|
|
|
@ -286,7 +286,8 @@ export class RepositoryStateCache {
|
|||
}
|
||||
|
||||
const oldState = pullRequestState.commitSelection
|
||||
const commitSelection = merge(oldState, fn(oldState))
|
||||
const commitSelection =
|
||||
oldState === null ? null : merge(oldState, fn(oldState))
|
||||
this.updatePullRequestState(repository, () => ({
|
||||
commitSelection,
|
||||
}))
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
terminateDesktopNotifications,
|
||||
} from './notifications'
|
||||
import { addTrustedIPCSender } from './trusted-ipc-sender'
|
||||
import { enablePreventClosingWhileUpdating } from '../lib/feature-flag'
|
||||
|
||||
export class AppWindow {
|
||||
private window: Electron.BrowserWindow
|
||||
|
@ -33,6 +34,7 @@ export class AppWindow {
|
|||
|
||||
private _loadTime: number | null = null
|
||||
private _rendererReadyTime: number | null = null
|
||||
private isDownloadingUpdate: boolean = false
|
||||
|
||||
private minWidth = 960
|
||||
private minHeight = 660
|
||||
|
@ -86,6 +88,7 @@ export class AppWindow {
|
|||
this.shouldMaximizeOnShow = savedWindowState.isMaximized
|
||||
|
||||
let quitting = false
|
||||
let quittingEvenIfUpdating = false
|
||||
app.on('before-quit', () => {
|
||||
quitting = true
|
||||
})
|
||||
|
@ -95,7 +98,40 @@ export class AppWindow {
|
|||
event.returnValue = true
|
||||
})
|
||||
|
||||
ipcMain.on('will-quit-even-if-updating', event => {
|
||||
quitting = true
|
||||
quittingEvenIfUpdating = true
|
||||
event.returnValue = true
|
||||
})
|
||||
|
||||
ipcMain.on('cancel-quitting', event => {
|
||||
quitting = false
|
||||
quittingEvenIfUpdating = false
|
||||
event.returnValue = true
|
||||
})
|
||||
|
||||
this.window.on('close', e => {
|
||||
// On macOS, closing the window doesn't mean the app is quitting. If the
|
||||
// app is updating, we will prevent the window from closing only when the
|
||||
// app is also quitting.
|
||||
if (
|
||||
enablePreventClosingWhileUpdating() &&
|
||||
(!__DARWIN__ || quitting) &&
|
||||
!quittingEvenIfUpdating &&
|
||||
this.isDownloadingUpdate
|
||||
) {
|
||||
e.preventDefault()
|
||||
ipcWebContents.send(this.window.webContents, 'show-installing-update')
|
||||
|
||||
// Make sure the window is visible, so the user can see why we're
|
||||
// preventing the app from quitting. This is important on macOS, where
|
||||
// the window could be hidden/closed when the user tries to quit.
|
||||
// It could also happen on Windows if the user quits the app from the
|
||||
// task bar while it's in the background.
|
||||
this.show()
|
||||
return
|
||||
}
|
||||
|
||||
// on macOS, when the user closes the window we really just hide it. This
|
||||
// lets us activate quickly and keep all our interesting logic in the
|
||||
// renderer.
|
||||
|
@ -104,9 +140,9 @@ export class AppWindow {
|
|||
// https://github.com/desktop/desktop/issues/12838
|
||||
if (this.window.isFullScreen()) {
|
||||
this.window.setFullScreen(false)
|
||||
this.window.once('leave-full-screen', () => app.hide())
|
||||
this.window.once('leave-full-screen', () => this.window.hide())
|
||||
} else {
|
||||
app.hide()
|
||||
this.window.hide()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -213,7 +249,7 @@ export class AppWindow {
|
|||
return !!this.loadTime && !!this.rendererReadyTime
|
||||
}
|
||||
|
||||
public onClose(fn: () => void) {
|
||||
public onClosed(fn: () => void) {
|
||||
this.window.on('closed', fn)
|
||||
}
|
||||
|
||||
|
@ -344,10 +380,12 @@ export class AppWindow {
|
|||
|
||||
public setupAutoUpdater() {
|
||||
autoUpdater.on('error', (error: Error) => {
|
||||
this.isDownloadingUpdate = false
|
||||
ipcWebContents.send(this.window.webContents, 'auto-updater-error', error)
|
||||
})
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
this.isDownloadingUpdate = false
|
||||
ipcWebContents.send(
|
||||
this.window.webContents,
|
||||
'auto-updater-checking-for-update'
|
||||
|
@ -355,6 +393,7 @@ export class AppWindow {
|
|||
})
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
this.isDownloadingUpdate = true
|
||||
ipcWebContents.send(
|
||||
this.window.webContents,
|
||||
'auto-updater-update-available'
|
||||
|
@ -362,6 +401,7 @@ export class AppWindow {
|
|||
})
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
this.isDownloadingUpdate = false
|
||||
ipcWebContents.send(
|
||||
this.window.webContents,
|
||||
'auto-updater-update-not-available'
|
||||
|
@ -369,6 +409,7 @@ export class AppWindow {
|
|||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
this.isDownloadingUpdate = false
|
||||
ipcWebContents.send(
|
||||
this.window.webContents,
|
||||
'auto-updater-update-downloaded'
|
||||
|
|
|
@ -490,6 +490,8 @@ app.on('ready', () => {
|
|||
mainWindow?.quitAndInstallUpdate()
|
||||
)
|
||||
|
||||
ipcMain.on('quit-app', () => app.quit())
|
||||
|
||||
ipcMain.on('minimize-window', () => mainWindow?.minimizeWindow())
|
||||
|
||||
ipcMain.on('maximize-window', () => mainWindow?.maximizeWindow())
|
||||
|
@ -738,7 +740,7 @@ function createWindow() {
|
|||
}
|
||||
}
|
||||
|
||||
window.onClose(() => {
|
||||
window.onClosed(() => {
|
||||
mainWindow = null
|
||||
if (!__DARWIN__ && !preventQuit) {
|
||||
app.quit()
|
||||
|
|
|
@ -285,6 +285,12 @@ export function buildDefaultMenu({
|
|||
accelerator: 'CmdOrCtrl+Shift+P',
|
||||
click: emit('pull'),
|
||||
},
|
||||
{
|
||||
id: 'fetch',
|
||||
label: __DARWIN__ ? 'Fetch' : '&Fetch',
|
||||
accelerator: 'CmdOrCtrl+Shift+T',
|
||||
click: emit('fetch'),
|
||||
},
|
||||
{
|
||||
label: removeRepoLabel,
|
||||
id: 'remove-repository',
|
||||
|
@ -428,12 +434,12 @@ export function buildDefaultMenu({
|
|||
},
|
||||
]
|
||||
|
||||
if (!hasCurrentPullRequest && enableStartingPullRequests()) {
|
||||
if (enableStartingPullRequests()) {
|
||||
branchSubmenu.push({
|
||||
label: __DARWIN__ ? 'Start Pull Request' : 'Start pull request',
|
||||
id: 'start-pull-request',
|
||||
label: __DARWIN__ ? 'Preview Pull Request' : 'Preview pull request',
|
||||
id: 'preview-pull-request',
|
||||
accelerator: 'CmdOrCtrl+Alt+P',
|
||||
click: emit('start-pull-request'),
|
||||
click: emit('preview-pull-request'),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -551,6 +557,10 @@ export function buildDefaultMenu({
|
|||
label: 'Pull Request Check Run Failed',
|
||||
click: emit('pull-request-check-run-failed'),
|
||||
},
|
||||
{
|
||||
label: 'Show App Error',
|
||||
click: emit('show-app-error'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -640,7 +650,7 @@ function emit(name: MenuEvent): ClickHandler {
|
|||
}
|
||||
|
||||
/** The zoom steps that we support, these factors must sorted */
|
||||
const ZoomInFactors = [1, 1.1, 1.25, 1.5, 1.75, 2]
|
||||
const ZoomInFactors = [0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2]
|
||||
const ZoomOutFactors = ZoomInFactors.slice().reverse()
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,7 @@ export type MenuEvent =
|
|||
| 'push'
|
||||
| 'force-push'
|
||||
| 'pull'
|
||||
| 'fetch'
|
||||
| 'show-changes'
|
||||
| 'show-history'
|
||||
| 'add-local-repository'
|
||||
|
@ -42,4 +43,5 @@ export type MenuEvent =
|
|||
| 'find-text'
|
||||
| 'create-issue-in-repository-on-github'
|
||||
| 'pull-request-check-run-failed'
|
||||
| 'start-pull-request'
|
||||
| 'preview-pull-request'
|
||||
| 'show-app-error'
|
||||
|
|
|
@ -35,4 +35,4 @@ export type MenuIDs =
|
|||
| 'compare-to-branch'
|
||||
| 'toggle-stashed-changes'
|
||||
| 'create-issue-in-repository-on-github'
|
||||
| 'start-pull-request'
|
||||
| 'preview-pull-request'
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Commit, CommitOneLine, ICommitContext } from './commit'
|
|||
import { IStashEntry } from './stash-entry'
|
||||
import { Account } from '../models/account'
|
||||
import { Progress } from './progress'
|
||||
import { ITextDiff, DiffSelection } from './diff'
|
||||
import { ITextDiff, DiffSelection, ImageDiffType } from './diff'
|
||||
import { RepositorySettingsTab } from '../ui/repository-settings/repository-settings'
|
||||
import { ICommitMessage } from './commit-message'
|
||||
import { IAuthor } from './author'
|
||||
|
@ -24,72 +24,81 @@ import { ValidNotificationPullRequestReview } from '../lib/valid-notification-pu
|
|||
import { UnreachableCommitsTab } from '../ui/history/unreachable-commits-dialog'
|
||||
|
||||
export enum PopupType {
|
||||
RenameBranch = 1,
|
||||
DeleteBranch,
|
||||
DeleteRemoteBranch,
|
||||
ConfirmDiscardChanges,
|
||||
Preferences,
|
||||
RepositorySettings,
|
||||
AddRepository,
|
||||
CreateRepository,
|
||||
CloneRepository,
|
||||
CreateBranch,
|
||||
SignIn,
|
||||
About,
|
||||
InstallGit,
|
||||
PublishRepository,
|
||||
Acknowledgements,
|
||||
UntrustedCertificate,
|
||||
RemoveRepository,
|
||||
TermsAndConditions,
|
||||
PushBranchCommits,
|
||||
CLIInstalled,
|
||||
GenericGitAuthentication,
|
||||
ExternalEditorFailed,
|
||||
OpenShellFailed,
|
||||
InitializeLFS,
|
||||
LFSAttributeMismatch,
|
||||
UpstreamAlreadyExists,
|
||||
ReleaseNotes,
|
||||
DeletePullRequest,
|
||||
OversizedFiles,
|
||||
CommitConflictsWarning,
|
||||
PushNeedsPull,
|
||||
ConfirmForcePush,
|
||||
StashAndSwitchBranch,
|
||||
ConfirmOverwriteStash,
|
||||
ConfirmDiscardStash,
|
||||
CreateTutorialRepository,
|
||||
ConfirmExitTutorial,
|
||||
PushRejectedDueToMissingWorkflowScope,
|
||||
SAMLReauthRequired,
|
||||
CreateFork,
|
||||
CreateTag,
|
||||
DeleteTag,
|
||||
LocalChangesOverwritten,
|
||||
ChooseForkSettings,
|
||||
ConfirmDiscardSelection,
|
||||
MoveToApplicationsFolder,
|
||||
ChangeRepositoryAlias,
|
||||
ThankYou,
|
||||
CommitMessage,
|
||||
MultiCommitOperation,
|
||||
WarnLocalChangesBeforeUndo,
|
||||
WarningBeforeReset,
|
||||
InvalidatedToken,
|
||||
AddSSHHost,
|
||||
SSHKeyPassphrase,
|
||||
SSHUserPassword,
|
||||
PullRequestChecksFailed,
|
||||
CICheckRunRerun,
|
||||
WarnForcePush,
|
||||
DiscardChangesRetry,
|
||||
PullRequestReview,
|
||||
UnreachableCommits,
|
||||
StartPullRequest,
|
||||
RenameBranch = 'RenameBranch',
|
||||
DeleteBranch = 'DeleteBranch',
|
||||
DeleteRemoteBranch = 'DeleteRemoteBranch',
|
||||
ConfirmDiscardChanges = 'ConfirmDiscardChanges',
|
||||
Preferences = 'Preferences',
|
||||
RepositorySettings = 'RepositorySettings',
|
||||
AddRepository = 'AddRepository',
|
||||
CreateRepository = 'CreateRepository',
|
||||
CloneRepository = 'CloneRepository',
|
||||
CreateBranch = 'CreateBranch',
|
||||
SignIn = 'SignIn',
|
||||
About = 'About',
|
||||
InstallGit = 'InstallGit',
|
||||
PublishRepository = 'PublishRepository',
|
||||
Acknowledgements = 'Acknowledgements',
|
||||
UntrustedCertificate = 'UntrustedCertificate',
|
||||
RemoveRepository = 'RemoveRepository',
|
||||
TermsAndConditions = 'TermsAndConditions',
|
||||
PushBranchCommits = 'PushBranchCommits',
|
||||
CLIInstalled = 'CLIInstalled',
|
||||
GenericGitAuthentication = 'GenericGitAuthentication',
|
||||
ExternalEditorFailed = 'ExternalEditorFailed',
|
||||
OpenShellFailed = 'OpenShellFailed',
|
||||
InitializeLFS = 'InitializeLFS',
|
||||
LFSAttributeMismatch = 'LFSAttributeMismatch',
|
||||
UpstreamAlreadyExists = 'UpstreamAlreadyExists',
|
||||
ReleaseNotes = 'ReleaseNotes',
|
||||
DeletePullRequest = 'DeletePullRequest',
|
||||
OversizedFiles = 'OversizedFiles',
|
||||
CommitConflictsWarning = 'CommitConflictsWarning',
|
||||
PushNeedsPull = 'PushNeedsPull',
|
||||
ConfirmForcePush = 'ConfirmForcePush',
|
||||
StashAndSwitchBranch = 'StashAndSwitchBranch',
|
||||
ConfirmOverwriteStash = 'ConfirmOverwriteStash',
|
||||
ConfirmDiscardStash = 'ConfirmDiscardStash',
|
||||
CreateTutorialRepository = 'CreateTutorialRepository',
|
||||
ConfirmExitTutorial = 'ConfirmExitTutorial',
|
||||
PushRejectedDueToMissingWorkflowScope = 'PushRejectedDueToMissingWorkflowScope',
|
||||
SAMLReauthRequired = 'SAMLReauthRequired',
|
||||
CreateFork = 'CreateFork',
|
||||
CreateTag = 'CreateTag',
|
||||
DeleteTag = 'DeleteTag',
|
||||
LocalChangesOverwritten = 'LocalChangesOverwritten',
|
||||
ChooseForkSettings = 'ChooseForkSettings',
|
||||
ConfirmDiscardSelection = 'ConfirmDiscardSelection',
|
||||
MoveToApplicationsFolder = 'MoveToApplicationsFolder',
|
||||
ChangeRepositoryAlias = 'ChangeRepositoryAlias',
|
||||
ThankYou = 'ThankYou',
|
||||
CommitMessage = 'CommitMessage',
|
||||
MultiCommitOperation = 'MultiCommitOperation',
|
||||
WarnLocalChangesBeforeUndo = 'WarnLocalChangesBeforeUndo',
|
||||
WarningBeforeReset = 'WarningBeforeReset',
|
||||
InvalidatedToken = 'InvalidatedToken',
|
||||
AddSSHHost = 'AddSSHHost',
|
||||
SSHKeyPassphrase = 'SSHKeyPassphrase',
|
||||
SSHUserPassword = 'SSHUserPassword',
|
||||
PullRequestChecksFailed = 'PullRequestChecksFailed',
|
||||
CICheckRunRerun = 'CICheckRunRerun',
|
||||
WarnForcePush = 'WarnForcePush',
|
||||
DiscardChangesRetry = 'DiscardChangesRetry',
|
||||
PullRequestReview = 'PullRequestReview',
|
||||
UnreachableCommits = 'UnreachableCommits',
|
||||
StartPullRequest = 'StartPullRequest',
|
||||
Error = 'Error',
|
||||
InstallingUpdate = 'InstallingUpdate',
|
||||
}
|
||||
|
||||
export type Popup =
|
||||
interface IBasePopup {
|
||||
/**
|
||||
* Unique id of the popup that it receives upon adding to the stack.
|
||||
*/
|
||||
readonly id?: string
|
||||
}
|
||||
|
||||
export type PopupDetail =
|
||||
| { type: PopupType.RenameBranch; repository: Repository; branch: Branch }
|
||||
| {
|
||||
type: PopupType.DeleteBranch
|
||||
|
@ -362,4 +371,23 @@ export type Popup =
|
|||
}
|
||||
| {
|
||||
type: PopupType.StartPullRequest
|
||||
prBaseBranches: ReadonlyArray<Branch>
|
||||
currentBranch: Branch
|
||||
defaultBranch: Branch | null
|
||||
externalEditorLabel?: string
|
||||
imageDiffType: ImageDiffType
|
||||
prRecentBaseBranches: ReadonlyArray<Branch>
|
||||
repository: Repository
|
||||
nonLocalCommitSHA: string | null
|
||||
showSideBySideDiff: boolean
|
||||
currentBranchHasPullRequest: boolean
|
||||
}
|
||||
| {
|
||||
type: PopupType.Error
|
||||
error: Error
|
||||
}
|
||||
| {
|
||||
type: PopupType.InstallingUpdate
|
||||
}
|
||||
|
||||
export type Popup = IBasePopup & PopupDetail
|
||||
|
|
|
@ -41,3 +41,12 @@ export class PullRequest {
|
|||
public readonly body: string
|
||||
) {}
|
||||
}
|
||||
|
||||
/** The types of pull request suggested next actions */
|
||||
export enum PullRequestSuggestedNextAction {
|
||||
PreviewPullRequest = 'PreviewPullRequest',
|
||||
CreatePullRequest = 'CreatePullRequest',
|
||||
}
|
||||
|
||||
export const defaultPullRequestSuggestedNextAction =
|
||||
PullRequestSuggestedNextAction.PreviewPullRequest
|
||||
|
|
|
@ -213,3 +213,15 @@ export function getForkContributionTarget(
|
|||
? repository.workflowPreferences.forkContributionTarget
|
||||
: ForkContributionTarget.Parent
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the fork is contributing to the parent
|
||||
*/
|
||||
export function isForkedRepositoryContributingToParent(
|
||||
repository: Repository
|
||||
): boolean {
|
||||
return (
|
||||
isRepositoryWithForkedGitHubRepository(repository) &&
|
||||
getForkContributionTarget(repository) === ForkContributionTarget.Parent
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { RelativeTime } from '../relative-time'
|
|||
import { assertNever } from '../../lib/fatal-error'
|
||||
import { ReleaseNotesUri } from '../lib/releases'
|
||||
import { encodePathAsUrl } from '../../lib/path'
|
||||
import { isTopMostDialog } from '../dialog/is-top-most'
|
||||
|
||||
const logoPath = __DARWIN__
|
||||
? 'static/logo-64x64@2x.png'
|
||||
|
@ -54,6 +55,9 @@ interface IAboutProps {
|
|||
|
||||
/** A function to call when the user wants to see Terms and Conditions. */
|
||||
readonly onShowTermsAndConditions: () => void
|
||||
|
||||
/** Whether the dialog is the top most in the dialog stack */
|
||||
readonly isTopMost: boolean
|
||||
}
|
||||
|
||||
interface IAboutState {
|
||||
|
@ -67,6 +71,16 @@ interface IAboutState {
|
|||
*/
|
||||
export class About extends React.Component<IAboutProps, IAboutState> {
|
||||
private updateStoreEventHandle: Disposable | null = null
|
||||
private checkIsTopMostDialog = isTopMostDialog(
|
||||
() => {
|
||||
window.addEventListener('keydown', this.onKeyDown)
|
||||
window.addEventListener('keyup', this.onKeyUp)
|
||||
},
|
||||
() => {
|
||||
window.removeEventListener('keydown', this.onKeyDown)
|
||||
window.removeEventListener('keyup', this.onKeyUp)
|
||||
}
|
||||
)
|
||||
|
||||
public constructor(props: IAboutProps) {
|
||||
super(props)
|
||||
|
@ -86,8 +100,11 @@ export class About extends React.Component<IAboutProps, IAboutState> {
|
|||
this.onUpdateStateChanged
|
||||
)
|
||||
this.setState({ updateState: updateStore.state })
|
||||
window.addEventListener('keydown', this.onKeyDown)
|
||||
window.addEventListener('keyup', this.onKeyUp)
|
||||
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||
}
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -95,8 +112,7 @@ export class About extends React.Component<IAboutProps, IAboutState> {
|
|||
this.updateStoreEventHandle.dispose()
|
||||
this.updateStoreEventHandle = null
|
||||
}
|
||||
window.removeEventListener('keydown', this.onKeyDown)
|
||||
window.removeEventListener('keyup', this.onKeyUp)
|
||||
this.checkIsTopMostDialog(false)
|
||||
}
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import * as OcticonSymbol from '../octicons/octicons.generated'
|
|||
import { LinkButton } from '../lib/link-button'
|
||||
import { PopupType } from '../../models/popup'
|
||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { FoldoutType } from '../../lib/app-state'
|
||||
|
||||
import untildify from 'untildify'
|
||||
import { showOpenDialog } from '../main-process-proxy'
|
||||
|
@ -265,6 +266,7 @@ export class AddExistingRepository extends React.Component<
|
|||
const repositories = await dispatcher.addRepositories([resolvedPath])
|
||||
|
||||
if (repositories.length > 0) {
|
||||
dispatcher.closeFoldout(FoldoutType.Repository)
|
||||
dispatcher.selectRepository(repositories[0])
|
||||
dispatcher.recordAddExistingRepository()
|
||||
}
|
||||
|
|
|
@ -34,7 +34,9 @@ import { showOpenDialog } from '../main-process-proxy'
|
|||
import { pathExists } from '../lib/path-exists'
|
||||
import { mkdir } from 'fs/promises'
|
||||
import { directoryExists } from '../../lib/directory-exists'
|
||||
import { FoldoutType } from '../../lib/app-state'
|
||||
import { join } from 'path'
|
||||
import { isTopMostDialog } from '../dialog/is-top-most'
|
||||
|
||||
/** The sentinel value used to indicate no gitignore should be used. */
|
||||
const NoGitIgnoreValue = 'None'
|
||||
|
@ -70,6 +72,9 @@ interface ICreateRepositoryProps {
|
|||
|
||||
/** Prefills path input so user doesn't have to. */
|
||||
readonly initialPath?: string
|
||||
|
||||
/** Whether the dialog is the top most in the dialog stack */
|
||||
readonly isTopMost: boolean
|
||||
}
|
||||
|
||||
interface ICreateRepositoryState {
|
||||
|
@ -114,6 +119,16 @@ export class CreateRepository extends React.Component<
|
|||
ICreateRepositoryProps,
|
||||
ICreateRepositoryState
|
||||
> {
|
||||
private checkIsTopMostDialog = isTopMostDialog(
|
||||
() => {
|
||||
this.updateReadMeExists(this.state.path, this.state.name)
|
||||
window.addEventListener('focus', this.onWindowFocus)
|
||||
},
|
||||
() => {
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
}
|
||||
)
|
||||
|
||||
public constructor(props: ICreateRepositoryProps) {
|
||||
super(props)
|
||||
|
||||
|
@ -144,7 +159,7 @@ export class CreateRepository extends React.Component<
|
|||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
window.addEventListener('focus', this.onWindowFocus)
|
||||
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||
|
||||
const gitIgnoreNames = await getGitIgnoreNames()
|
||||
const licenses = await getLicenses()
|
||||
|
@ -157,8 +172,12 @@ export class CreateRepository extends React.Component<
|
|||
this.updateReadMeExists(path, this.state.name)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
public componentDidUpdate(): void {
|
||||
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.checkIsTopMostDialog(false)
|
||||
}
|
||||
|
||||
private initializePath = async () => {
|
||||
|
@ -391,6 +410,7 @@ export class CreateRepository extends React.Component<
|
|||
|
||||
this.updateDefaultDirectory()
|
||||
|
||||
this.props.dispatcher.closeFoldout(FoldoutType.Repository)
|
||||
this.props.dispatcher.selectRepository(repository)
|
||||
this.props.dispatcher.recordCreateRepository()
|
||||
this.props.onDismissed()
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
import { dialogTransitionTimeout } from './app'
|
||||
import { GitError, isAuthFailureError } from '../lib/git/core'
|
||||
import { Popup, PopupType } from '../models/popup'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { OkCancelButtonGroup } from './dialog/ok-cancel-button-group'
|
||||
import { ErrorWithMetadata } from '../lib/error-with-metadata'
|
||||
import { RetryActionType, RetryAction } from '../models/retry-actions'
|
||||
|
@ -18,14 +17,11 @@ import memoizeOne from 'memoize-one'
|
|||
import { parseCarriageReturn } from '../lib/parse-carriage-return'
|
||||
|
||||
interface IAppErrorProps {
|
||||
/** The list of queued, app-wide, errors */
|
||||
readonly errors: ReadonlyArray<Error>
|
||||
/** The error to be displayed */
|
||||
readonly error: Error
|
||||
|
||||
/**
|
||||
* A callback which is used whenever a particular error
|
||||
* has been shown to, and been dismissed by, the user.
|
||||
*/
|
||||
readonly onClearError: (error: Error) => void
|
||||
/** Called to dismiss the dialog */
|
||||
readonly onDismissed: () => void
|
||||
readonly onShowPopup: (popupType: Popup) => void | undefined
|
||||
readonly onRetryAction: (retryAction: RetryAction) => void
|
||||
}
|
||||
|
@ -53,13 +49,13 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
|||
public constructor(props: IAppErrorProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
error: props.errors[0] || null,
|
||||
error: props.error,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: IAppErrorProps) {
|
||||
const error = nextProps.errors[0] || null
|
||||
const error = nextProps.error
|
||||
|
||||
// We keep the currently shown error until it has disappeared
|
||||
// from the first spot in the application error queue.
|
||||
|
@ -68,23 +64,8 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onDismissed = () => {
|
||||
const currentError = this.state.error
|
||||
|
||||
if (currentError !== null) {
|
||||
this.setState({ error: null, disabled: true })
|
||||
|
||||
// Give some time for the dialog to nicely transition
|
||||
// out before we clear the error and, potentially, deal
|
||||
// with the next error in the queue.
|
||||
window.setTimeout(() => {
|
||||
this.props.onClearError(currentError)
|
||||
}, dialogTransitionTimeout.exit)
|
||||
}
|
||||
}
|
||||
|
||||
private showPreferencesDialog = () => {
|
||||
this.onDismissed()
|
||||
this.props.onDismissed()
|
||||
|
||||
//This is a hacky solution to resolve multiple dialog windows
|
||||
//being open at the same time.
|
||||
|
@ -95,7 +76,7 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
|||
|
||||
private onRetryAction = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
this.onDismissed()
|
||||
this.props.onDismissed()
|
||||
|
||||
const { error } = this.state
|
||||
|
||||
|
@ -128,36 +109,6 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
|||
return 'Error'
|
||||
}
|
||||
|
||||
private renderDialog() {
|
||||
const error = this.state.error
|
||||
|
||||
if (!error) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id="app-error"
|
||||
type="error"
|
||||
key="error"
|
||||
title={this.getTitle(error)}
|
||||
dismissable={false}
|
||||
onSubmit={this.onDismissed}
|
||||
onDismissed={this.onDismissed}
|
||||
disabled={this.state.disabled}
|
||||
className={
|
||||
isRawGitError(this.state.error) ? 'raw-git-error' : undefined
|
||||
}
|
||||
>
|
||||
<DialogContent onRef={this.onDialogContentRef}>
|
||||
{this.renderErrorMessage(error)}
|
||||
{this.renderContentAfterErrorMessage(error)}
|
||||
</DialogContent>
|
||||
{this.renderFooter(error)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
private renderContentAfterErrorMessage(error: Error) {
|
||||
if (!isErrorWithMetaData(error)) {
|
||||
return undefined
|
||||
|
@ -207,7 +158,7 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
|||
|
||||
private onCloseButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
this.onDismissed()
|
||||
this.props.onDismissed()
|
||||
}
|
||||
|
||||
private renderFooter(error: Error) {
|
||||
|
@ -257,16 +208,32 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const dialogContent = this.renderDialog()
|
||||
const error = this.state.error
|
||||
|
||||
if (!error) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{dialogContent && (
|
||||
<CSSTransition classNames="modal" timeout={dialogTransitionTimeout}>
|
||||
{dialogContent}
|
||||
</CSSTransition>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
<Dialog
|
||||
id="app-error"
|
||||
type="error"
|
||||
key="error"
|
||||
title={this.getTitle(error)}
|
||||
dismissable={false}
|
||||
onSubmit={this.props.onDismissed}
|
||||
onDismissed={this.props.onDismissed}
|
||||
disabled={this.state.disabled}
|
||||
className={
|
||||
isRawGitError(this.state.error) ? 'raw-git-error' : undefined
|
||||
}
|
||||
>
|
||||
<DialogContent onRef={this.onDialogContentRef}>
|
||||
{this.renderErrorMessage(error)}
|
||||
{this.renderContentAfterErrorMessage(error)}
|
||||
</DialogContent>
|
||||
{this.renderFooter(error)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from 'react'
|
||||
import * as crypto from 'crypto'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import {
|
||||
IAppState,
|
||||
|
@ -14,6 +13,7 @@ import { assertNever } from '../lib/fatal-error'
|
|||
import { shell } from '../lib/app-shell'
|
||||
import { updateStore, UpdateStatus } from './lib/update-store'
|
||||
import { RetryAction } from '../models/retry-actions'
|
||||
import { FetchType } from '../models/fetch'
|
||||
import { shouldRenderApplicationMenu } from './lib/features'
|
||||
import { matchExistingRepository } from '../lib/repository-matching'
|
||||
import { getDotComAPIEndpoint } from '../lib/api'
|
||||
|
@ -93,7 +93,10 @@ import { RepositoryStateCache } from '../lib/stores/repository-state-cache'
|
|||
import { PopupType, Popup } from '../models/popup'
|
||||
import { OversizedFiles } from './changes/oversized-files-warning'
|
||||
import { PushNeedsPullWarning } from './push-needs-pull'
|
||||
import { isCurrentBranchForcePush } from '../lib/rebase'
|
||||
import {
|
||||
ForcePushBranchState,
|
||||
getCurrentBranchForcePushState,
|
||||
} from '../lib/rebase'
|
||||
import { Banner, BannerType } from '../models/banner'
|
||||
import { StashAndSwitchBranch } from './stash-changes/stash-and-switch-branch-dialog'
|
||||
import { OverwriteStash } from './stash-changes/overwrite-stashed-changes-dialog'
|
||||
|
@ -158,6 +161,12 @@ import { SSHUserPassword } from './ssh/ssh-user-password'
|
|||
import { showContextualMenu } from '../lib/menu-item'
|
||||
import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog'
|
||||
import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-dialog'
|
||||
import { sendNonFatalException } from '../lib/helpers/non-fatal-exception'
|
||||
import { createCommitURL } from '../lib/commit-url'
|
||||
import { uuid } from '../lib/uuid'
|
||||
import { InstallingUpdate } from './installing-update/installing-update'
|
||||
import { enableStackedPopups } from '../lib/feature-flag'
|
||||
import { DialogStackContext } from './dialog'
|
||||
|
||||
const MinuteInMilliseconds = 1000 * 60
|
||||
const HourInMilliseconds = MinuteInMilliseconds * 60
|
||||
|
@ -214,7 +223,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
* modal dialog such as the preferences, or an error dialog.
|
||||
*/
|
||||
private get isShowingModal() {
|
||||
return this.state.currentPopup !== null || this.state.errors.length > 0
|
||||
return this.state.currentPopup !== null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -222,8 +231,8 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
* passed popupType, so it can be used in render() without creating
|
||||
* multiple instances when the component gets re-rendered.
|
||||
*/
|
||||
private getOnPopupDismissedFn = memoizeOne((popupType: PopupType) => {
|
||||
return () => this.onPopupDismissed(popupType)
|
||||
private getOnPopupDismissedFn = memoizeOne((popupId: string) => {
|
||||
return () => this.onPopupDismissed(popupId)
|
||||
})
|
||||
|
||||
public constructor(props: IAppProps) {
|
||||
|
@ -278,7 +287,14 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
updateStore.onError(error => {
|
||||
log.error(`Error checking for updates`, error)
|
||||
|
||||
this.props.dispatcher.postError(error)
|
||||
// It is possible to obtain an error with no message. This was found to be
|
||||
// the case on a windows instance where there was not space on the hard
|
||||
// drive to download the installer. In this case, we want to override the
|
||||
// error message so the user is not given a blank dialog.
|
||||
const hasErrorMsg = error.message.trim().length > 0
|
||||
this.props.dispatcher.postError(
|
||||
hasErrorMsg ? error : new Error('Checking for updates failed.')
|
||||
)
|
||||
})
|
||||
|
||||
ipcRenderer.on('launch-timing-stats', (_, stats) => {
|
||||
|
@ -346,7 +362,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
|
||||
private onMenuEvent(name: MenuEvent): any {
|
||||
// Don't react to menu events when an error dialog is shown.
|
||||
if (this.state.errors.length) {
|
||||
if (name !== 'show-app-error' && this.state.errorCount > 1) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -357,6 +373,8 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return this.push({ forceWithLease: true })
|
||||
case 'pull':
|
||||
return this.pull()
|
||||
case 'fetch':
|
||||
return this.fetch()
|
||||
case 'show-changes':
|
||||
return this.showChanges()
|
||||
case 'show-history':
|
||||
|
@ -421,7 +439,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return this.goToCommitMessage()
|
||||
case 'open-pull-request':
|
||||
return this.openPullRequest()
|
||||
case 'start-pull-request':
|
||||
case 'preview-pull-request':
|
||||
return this.startPullRequest()
|
||||
case 'install-cli':
|
||||
return this.props.dispatcher.installCLI()
|
||||
|
@ -443,6 +461,10 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return this.findText()
|
||||
case 'pull-request-check-run-failed':
|
||||
return this.testPullRequestCheckRunFailed()
|
||||
case 'show-app-error':
|
||||
return this.props.dispatcher.postError(
|
||||
new Error('Test Error - to use default error handler' + uuid())
|
||||
)
|
||||
default:
|
||||
return assertNever(name, `Unknown menu event name: ${name}`)
|
||||
}
|
||||
|
@ -948,6 +970,15 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
this.props.dispatcher.pull(state.repository)
|
||||
}
|
||||
|
||||
private async fetch() {
|
||||
const state = this.state.selectedState
|
||||
if (state == null || state.type !== SelectionType.Repository) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.dispatcher.fetch(state.repository, FetchType.UserInitiatedTask)
|
||||
}
|
||||
|
||||
private showStashedChanges() {
|
||||
const state = this.state.selectedState
|
||||
if (state == null || state.type !== SelectionType.Repository) {
|
||||
|
@ -1352,8 +1383,8 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
)
|
||||
}
|
||||
|
||||
private onPopupDismissed = (popupType: PopupType) => {
|
||||
return this.props.dispatcher.closePopup(popupType)
|
||||
private onPopupDismissed = (popupId: string) => {
|
||||
return this.props.dispatcher.closePopupById(popupId)
|
||||
}
|
||||
|
||||
private onContinueWithUntrustedCertificate = (
|
||||
|
@ -1368,19 +1399,44 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
private onUpdateAvailableDismissed = () =>
|
||||
this.props.dispatcher.setUpdateBannerVisibility(false)
|
||||
|
||||
private currentPopupContent(): JSX.Element | null {
|
||||
// Hide any dialogs while we're displaying an error
|
||||
if (this.state.errors.length) {
|
||||
private allPopupContent(): JSX.Element | null {
|
||||
let { allPopups } = this.state
|
||||
|
||||
if (!enableStackedPopups() && this.state.currentPopup !== null) {
|
||||
allPopups = [this.state.currentPopup]
|
||||
}
|
||||
|
||||
if (allPopups.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const popup = this.state.currentPopup
|
||||
return (
|
||||
<>
|
||||
{allPopups.map(popup => {
|
||||
const isTopMost = this.state.currentPopup?.id === popup.id
|
||||
return (
|
||||
<DialogStackContext.Provider key={popup.id} value={{ isTopMost }}>
|
||||
{this.popupContent(popup, isTopMost)}
|
||||
</DialogStackContext.Provider>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (!popup) {
|
||||
private popupContent(popup: Popup, isTopMost: boolean): JSX.Element | null {
|
||||
if (popup.id === undefined) {
|
||||
// Should not be possible... but if it does we want to know about it.
|
||||
sendNonFatalException(
|
||||
'PopupNoId',
|
||||
new Error(
|
||||
`Attempted to open a popup of type '${popup.type}' without an Id`
|
||||
)
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const onPopupDismissedFn = this.getOnPopupDismissedFn(popup.type)
|
||||
const onPopupDismissedFn = this.getOnPopupDismissedFn(popup.id)
|
||||
|
||||
switch (popup.type) {
|
||||
case PopupType.RenameBranch:
|
||||
|
@ -1481,6 +1537,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
confirmDiscardChangesPermanently={
|
||||
this.state.askForConfirmationOnDiscardChangesPermanently
|
||||
}
|
||||
confirmDiscardStash={this.state.askForConfirmationOnDiscardStash}
|
||||
confirmForcePush={this.state.askForConfirmationOnForcePush}
|
||||
confirmUndoCommit={this.state.askForConfirmationOnUndoCommit}
|
||||
uncommittedChangesStrategy={this.state.uncommittedChangesStrategy}
|
||||
|
@ -1542,6 +1599,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
onDismissed={onPopupDismissedFn}
|
||||
dispatcher={this.props.dispatcher}
|
||||
initialPath={popup.path}
|
||||
isTopMost={isTopMost}
|
||||
/>
|
||||
)
|
||||
case PopupType.CloneRepository:
|
||||
|
@ -1557,6 +1615,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
onTabSelected={this.onCloneRepositoriesTabSelected}
|
||||
apiRepositories={this.state.apiRepositories}
|
||||
onRefreshRepositories={this.onRefreshRepositories}
|
||||
isTopMost={isTopMost}
|
||||
/>
|
||||
)
|
||||
case PopupType.CreateBranch: {
|
||||
|
@ -1617,6 +1676,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
onCheckForNonStaggeredUpdates={this.onCheckForNonStaggeredUpdates}
|
||||
onShowAcknowledgements={this.showAcknowledgements}
|
||||
onShowTermsAndConditions={this.showTermsAndConditions}
|
||||
isTopMost={isTopMost}
|
||||
/>
|
||||
)
|
||||
case PopupType.PublishRepository:
|
||||
|
@ -1849,6 +1909,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
<ConfirmDiscardStashDialog
|
||||
key="confirm-discard-stash-dialog"
|
||||
dispatcher={this.props.dispatcher}
|
||||
askForConfirmationOnDiscardStash={
|
||||
this.state.askForConfirmationOnDiscardStash
|
||||
}
|
||||
repository={repository}
|
||||
stash={stash}
|
||||
onDismissed={onPopupDismissedFn}
|
||||
|
@ -2243,36 +2306,73 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
)
|
||||
}
|
||||
case PopupType.StartPullRequest: {
|
||||
const { selectedState } = this.state
|
||||
if (
|
||||
selectedState == null ||
|
||||
selectedState.type !== SelectionType.Repository
|
||||
) {
|
||||
// Intentionally chose to get the current pull request state on
|
||||
// rerender because state variables such as file selection change
|
||||
// via the dispatcher.
|
||||
const pullRequestState = this.getPullRequestState()
|
||||
if (pullRequestState === null) {
|
||||
// This shouldn't happen..
|
||||
sendNonFatalException(
|
||||
'FailedToStartPullRequest',
|
||||
new Error(
|
||||
'Failed to start pull request because pull request state was null'
|
||||
)
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const { state: repoState, repository } = selectedState
|
||||
const { pullRequestState, branchesState } = repoState
|
||||
if (
|
||||
pullRequestState === null ||
|
||||
branchesState.tip.kind !== TipState.Valid
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const { allBranches, recentBranches, defaultBranch, tip } =
|
||||
branchesState
|
||||
const currentBranch = tip.branch
|
||||
const { pullRequestFilesListWidth, hideWhitespaceInPullRequestDiff } =
|
||||
this.state
|
||||
|
||||
const {
|
||||
prBaseBranches,
|
||||
currentBranch,
|
||||
defaultBranch,
|
||||
imageDiffType,
|
||||
externalEditorLabel,
|
||||
nonLocalCommitSHA,
|
||||
prRecentBaseBranches,
|
||||
repository,
|
||||
showSideBySideDiff,
|
||||
currentBranchHasPullRequest,
|
||||
} = popup
|
||||
|
||||
return (
|
||||
<OpenPullRequestDialog
|
||||
key="open-pull-request"
|
||||
allBranches={allBranches}
|
||||
prBaseBranches={prBaseBranches}
|
||||
currentBranch={currentBranch}
|
||||
defaultBranch={defaultBranch}
|
||||
dispatcher={this.props.dispatcher}
|
||||
fileListWidth={pullRequestFilesListWidth}
|
||||
hideWhitespaceInDiff={hideWhitespaceInPullRequestDiff}
|
||||
imageDiffType={imageDiffType}
|
||||
nonLocalCommitSHA={nonLocalCommitSHA}
|
||||
pullRequestState={pullRequestState}
|
||||
recentBranches={recentBranches}
|
||||
prRecentBaseBranches={prRecentBaseBranches}
|
||||
repository={repository}
|
||||
externalEditorLabel={externalEditorLabel}
|
||||
showSideBySideDiff={showSideBySideDiff}
|
||||
currentBranchHasPullRequest={currentBranchHasPullRequest}
|
||||
onDismissed={onPopupDismissedFn}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case PopupType.Error: {
|
||||
return (
|
||||
<AppError
|
||||
error={popup.error}
|
||||
onDismissed={onPopupDismissedFn}
|
||||
onShowPopup={this.showPopup}
|
||||
onRetryAction={this.onRetryAction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case PopupType.InstallingUpdate: {
|
||||
return (
|
||||
<InstallingUpdate
|
||||
key="installing-update"
|
||||
dispatcher={this.props.dispatcher}
|
||||
onDismissed={onPopupDismissedFn}
|
||||
/>
|
||||
)
|
||||
|
@ -2282,6 +2382,18 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
}
|
||||
}
|
||||
|
||||
private getPullRequestState() {
|
||||
const { selectedState } = this.state
|
||||
if (
|
||||
selectedState == null ||
|
||||
selectedState.type !== SelectionType.Repository
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return selectedState.state.pullRequestState
|
||||
}
|
||||
|
||||
private getWarnForcePushDialogOnBegin(
|
||||
onBegin: () => void,
|
||||
onPopupDismissedFn: () => void
|
||||
|
@ -2375,8 +2487,8 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
this.props.dispatcher.showPopup({ type: PopupType.TermsAndConditions })
|
||||
}
|
||||
|
||||
private renderPopup() {
|
||||
const popupContent = this.currentPopupContent()
|
||||
private renderPopups() {
|
||||
const popupContent = this.allPopupContent()
|
||||
|
||||
return (
|
||||
<TransitionGroup>
|
||||
|
@ -2430,8 +2542,6 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return <FullScreenInfo windowState={this.state.windowState} />
|
||||
}
|
||||
|
||||
private clearError = (error: Error) => this.props.dispatcher.clearError(error)
|
||||
|
||||
private onConfirmDiscardChangesChanged = (value: boolean) => {
|
||||
this.props.dispatcher.setConfirmDiscardChangesSetting(value)
|
||||
}
|
||||
|
@ -2440,17 +2550,6 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
this.props.dispatcher.setConfirmDiscardChangesPermanentlySetting(value)
|
||||
}
|
||||
|
||||
private renderAppError() {
|
||||
return (
|
||||
<AppError
|
||||
errors={this.state.errors}
|
||||
onClearError={this.clearError}
|
||||
onShowPopup={this.showPopup}
|
||||
onRetryAction={this.onRetryAction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private onRetryAction = (retryAction: RetryAction) => {
|
||||
this.props.dispatcher.performRetry(retryAction)
|
||||
}
|
||||
|
@ -2477,8 +2576,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
{this.renderToolbar()}
|
||||
{this.renderBanner()}
|
||||
{this.renderRepository()}
|
||||
{this.renderPopup()}
|
||||
{this.renderAppError()}
|
||||
{this.renderPopups()}
|
||||
{this.renderDragElement()}
|
||||
</div>
|
||||
)
|
||||
|
@ -2702,7 +2800,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
remoteName = tip.branch.upstreamRemoteName
|
||||
}
|
||||
|
||||
const isForcePush = isCurrentBranchForcePush(branchesState, aheadBehind)
|
||||
const isForcePush =
|
||||
getCurrentBranchForcePushState(branchesState, aheadBehind) ===
|
||||
ForcePushBranchState.Recommended
|
||||
|
||||
return (
|
||||
<PushPullButton
|
||||
|
@ -2955,6 +3055,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
askForConfirmationOnDiscardChanges={
|
||||
state.askForConfirmationOnDiscardChanges
|
||||
}
|
||||
askForConfirmationOnDiscardStash={
|
||||
state.askForConfirmationOnDiscardStash
|
||||
}
|
||||
accounts={state.accounts}
|
||||
externalEditorLabel={externalEditorLabel}
|
||||
resolvedExternalEditor={state.resolvedExternalEditor}
|
||||
|
@ -2967,6 +3070,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
aheadBehindStore={this.props.aheadBehindStore}
|
||||
commitSpellcheckEnabled={this.state.commitSpellcheckEnabled}
|
||||
onCherryPick={this.startCherryPickWithoutBranch}
|
||||
pullRequestSuggestedNextAction={state.pullRequestSuggestedNextAction}
|
||||
/>
|
||||
)
|
||||
} else if (selectedState.type === SelectionType.CloningRepository) {
|
||||
|
@ -3049,22 +3153,17 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return
|
||||
}
|
||||
|
||||
const baseURL = repository.gitHubRepository.htmlURL
|
||||
const commitURL = createCommitURL(
|
||||
repository.gitHubRepository,
|
||||
SHA,
|
||||
filePath
|
||||
)
|
||||
|
||||
let fileSuffix = ''
|
||||
if (filePath != null) {
|
||||
const fileHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(filePath)
|
||||
.digest('hex')
|
||||
fileSuffix = '#diff-' + fileHash
|
||||
if (commitURL === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (baseURL) {
|
||||
this.props.dispatcher.openInBrowser(
|
||||
`${baseURL}/commit/${SHA}${fileSuffix}`
|
||||
)
|
||||
}
|
||||
this.props.dispatcher.openInBrowser(commitURL)
|
||||
}
|
||||
|
||||
private onBranchDeleted = (repository: Repository) => {
|
||||
|
|
|
@ -110,6 +110,9 @@ interface IBranchListProps {
|
|||
|
||||
/** Called to render content before/above the branches filter and list. */
|
||||
readonly renderPreList?: () => JSX.Element | null
|
||||
|
||||
/** Optional: No branches message */
|
||||
readonly noBranchesMessage?: string | JSX.Element
|
||||
}
|
||||
|
||||
interface IBranchListState {
|
||||
|
@ -249,6 +252,7 @@ export class BranchList extends React.Component<
|
|||
<NoBranches
|
||||
onCreateNewBranch={this.onCreateNewBranch}
|
||||
canCreateNewBranch={this.props.canCreateNewBranch}
|
||||
noBranchesMessage={this.props.noBranchesMessage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
import * as React from 'react'
|
||||
import { IMatches } from '../../lib/fuzzy-find'
|
||||
import { Branch } from '../../models/branch'
|
||||
import { Button } from '../lib/button'
|
||||
import { ClickSource } from '../lib/list'
|
||||
import { Popover } from '../lib/popover'
|
||||
import { Ref } from '../lib/ref'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { PopoverDropdown } from '../lib/popover-dropdown'
|
||||
import { BranchList } from './branch-list'
|
||||
import { renderDefaultBranch } from './branch-renderer'
|
||||
import { IBranchListItem } from './group-branches'
|
||||
|
||||
const defaultDropdownListHeight = 300
|
||||
const maxDropdownListHeight = 500
|
||||
|
||||
interface IBranchSelectProps {
|
||||
/** The initially selected branch. */
|
||||
readonly branch: Branch
|
||||
readonly branch: Branch | null
|
||||
|
||||
/**
|
||||
* See IBranchesState.defaultBranch
|
||||
|
@ -40,13 +33,14 @@ interface IBranchSelectProps {
|
|||
|
||||
/** Called when the user changes the selected branch. */
|
||||
readonly onChange?: (branch: Branch) => void
|
||||
|
||||
/** Optional: No branches message */
|
||||
readonly noBranchesMessage?: string | JSX.Element
|
||||
}
|
||||
|
||||
interface IBranchSelectState {
|
||||
readonly showBranchDropdown: boolean
|
||||
readonly selectedBranch: Branch | null
|
||||
readonly filterText: string
|
||||
readonly dropdownListHeight: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,67 +50,25 @@ export class BranchSelect extends React.Component<
|
|||
IBranchSelectProps,
|
||||
IBranchSelectState
|
||||
> {
|
||||
private invokeButtonRef: HTMLButtonElement | null = null
|
||||
private popoverRef = React.createRef<PopoverDropdown>()
|
||||
|
||||
public constructor(props: IBranchSelectProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
showBranchDropdown: false,
|
||||
selectedBranch: props.branch,
|
||||
filterText: '',
|
||||
dropdownListHeight: defaultDropdownListHeight,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.calculateDropdownListHeight()
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.calculateDropdownListHeight()
|
||||
}
|
||||
|
||||
private calculateDropdownListHeight = () => {
|
||||
if (this.invokeButtonRef === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const windowHeight = window.innerHeight
|
||||
const bottomOfButton = this.invokeButtonRef.getBoundingClientRect().bottom
|
||||
const listHeaderHeight = 75
|
||||
const calcMaxHeight = Math.round(
|
||||
windowHeight - bottomOfButton - listHeaderHeight
|
||||
)
|
||||
|
||||
const dropdownListHeight =
|
||||
calcMaxHeight > maxDropdownListHeight
|
||||
? maxDropdownListHeight
|
||||
: calcMaxHeight
|
||||
if (dropdownListHeight !== this.state.dropdownListHeight) {
|
||||
this.setState({ dropdownListHeight })
|
||||
}
|
||||
}
|
||||
|
||||
private onInvokeButtonRef = (buttonRef: HTMLButtonElement | null) => {
|
||||
this.invokeButtonRef = buttonRef
|
||||
}
|
||||
|
||||
private toggleBranchDropdown = () => {
|
||||
this.setState({ showBranchDropdown: !this.state.showBranchDropdown })
|
||||
}
|
||||
|
||||
private closeBranchDropdown = () => {
|
||||
this.setState({ showBranchDropdown: false })
|
||||
}
|
||||
|
||||
private renderBranch = (item: IBranchListItem, matches: IMatches) => {
|
||||
return renderDefaultBranch(item, matches, this.props.currentBranch)
|
||||
}
|
||||
|
||||
private onItemClick = (branch: Branch, source: ClickSource) => {
|
||||
source.event.preventDefault()
|
||||
this.setState({ showBranchDropdown: false, selectedBranch: branch })
|
||||
this.popoverRef.current?.closePopover()
|
||||
this.setState({ selectedBranch: branch })
|
||||
this.props.onChange?.(branch)
|
||||
}
|
||||
|
||||
|
@ -124,67 +76,38 @@ export class BranchSelect extends React.Component<
|
|||
this.setState({ filterText })
|
||||
}
|
||||
|
||||
public renderBranchDropdown() {
|
||||
if (!this.state.showBranchDropdown) {
|
||||
return
|
||||
}
|
||||
|
||||
const { currentBranch, defaultBranch, recentBranches, allBranches } =
|
||||
this.props
|
||||
|
||||
const { filterText, selectedBranch, dropdownListHeight } = this.state
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="branch-select-dropdown"
|
||||
onClickOutside={this.closeBranchDropdown}
|
||||
>
|
||||
<div className="branch-select-dropdown-header">
|
||||
Choose a base branch
|
||||
<button
|
||||
className="close"
|
||||
onClick={this.closeBranchDropdown}
|
||||
aria-label="close"
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.x} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="branch-select-dropdown-list"
|
||||
style={{ height: `${dropdownListHeight}px` }}
|
||||
>
|
||||
<BranchList
|
||||
allBranches={allBranches}
|
||||
currentBranch={currentBranch}
|
||||
defaultBranch={defaultBranch}
|
||||
recentBranches={recentBranches}
|
||||
filterText={filterText}
|
||||
onFilterTextChanged={this.onFilterTextChanged}
|
||||
selectedBranch={selectedBranch}
|
||||
canCreateNewBranch={false}
|
||||
renderBranch={this.renderBranch}
|
||||
onItemClick={this.onItemClick}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
currentBranch,
|
||||
defaultBranch,
|
||||
recentBranches,
|
||||
allBranches,
|
||||
noBranchesMessage,
|
||||
} = this.props
|
||||
|
||||
const { filterText, selectedBranch } = this.state
|
||||
|
||||
return (
|
||||
<div className="branch-select-component">
|
||||
<Button
|
||||
onClick={this.toggleBranchDropdown}
|
||||
onButtonRef={this.onInvokeButtonRef}
|
||||
>
|
||||
<Ref>
|
||||
<span className="base-label">base:</span>
|
||||
{this.state.selectedBranch?.name}
|
||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
||||
</Ref>
|
||||
</Button>
|
||||
{this.renderBranchDropdown()}
|
||||
</div>
|
||||
<PopoverDropdown
|
||||
contentTitle="Choose a base branch"
|
||||
buttonContent={selectedBranch?.name ?? ''}
|
||||
label="base:"
|
||||
ref={this.popoverRef}
|
||||
>
|
||||
<BranchList
|
||||
allBranches={allBranches}
|
||||
currentBranch={currentBranch}
|
||||
defaultBranch={defaultBranch}
|
||||
recentBranches={recentBranches}
|
||||
filterText={filterText}
|
||||
onFilterTextChanged={this.onFilterTextChanged}
|
||||
selectedBranch={selectedBranch}
|
||||
canCreateNewBranch={false}
|
||||
renderBranch={this.renderBranch}
|
||||
onItemClick={this.onItemClick}
|
||||
noBranchesMessage={noBranchesMessage}
|
||||
/>
|
||||
</PopoverDropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ interface INoBranchesProps {
|
|||
readonly onCreateNewBranch: () => void
|
||||
/** True to display the UI elements for creating a new branch, false to hide them */
|
||||
readonly canCreateNewBranch: boolean
|
||||
/** Optional: No branches message */
|
||||
readonly noBranchesMessage?: string | JSX.Element
|
||||
}
|
||||
|
||||
export class NoBranches extends React.Component<INoBranchesProps> {
|
||||
|
@ -43,7 +45,11 @@ export class NoBranches extends React.Component<INoBranchesProps> {
|
|||
)
|
||||
}
|
||||
|
||||
return <div className="no-branches">Sorry, I can't find that branch</div>
|
||||
return (
|
||||
<div className="no-branches">
|
||||
{this.props.noBranchesMessage ?? "Sorry, I can't find that branch"}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderShortcut() {
|
||||
|
|
|
@ -6,7 +6,6 @@ import { Octicon, iconForStatus } from '../octicons'
|
|||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { mapStatus } from '../../lib/status'
|
||||
import { DiffOptions } from '../diff/diff-options'
|
||||
import { RepositorySectionTab } from '../../lib/app-state'
|
||||
|
||||
interface IChangedFileDetailsProps {
|
||||
readonly path: string
|
||||
|
@ -61,7 +60,7 @@ export class ChangedFileDetails extends React.Component<
|
|||
|
||||
return (
|
||||
<DiffOptions
|
||||
sourceTab={RepositorySectionTab.Changes}
|
||||
isInteractiveDiff={true}
|
||||
onHideWhitespaceChangesChanged={
|
||||
this.props.onHideWhitespaceInDiffChanged
|
||||
}
|
||||
|
|
|
@ -13,12 +13,21 @@ import { TipState, IValidBranch } from '../../models/tip'
|
|||
import { Ref } from '../lib/ref'
|
||||
import { IAheadBehind } from '../../models/branch'
|
||||
import { IRemote } from '../../models/remote'
|
||||
import { isCurrentBranchForcePush } from '../../lib/rebase'
|
||||
import {
|
||||
ForcePushBranchState,
|
||||
getCurrentBranchForcePushState,
|
||||
} from '../../lib/rebase'
|
||||
import { StashedChangesLoadStates } from '../../models/stash-entry'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import { SuggestedActionGroup } from '../suggested-actions'
|
||||
import { PreferencesTab } from '../../models/preferences'
|
||||
import { PopupType } from '../../models/popup'
|
||||
import {
|
||||
DropdownSuggestedAction,
|
||||
IDropdownSuggestedActionOption,
|
||||
} from '../suggested-actions/dropdown-suggested-action'
|
||||
import { PullRequestSuggestedNextAction } from '../../models/pull-request'
|
||||
import { enableStartingPullRequests } from '../../lib/feature-flag'
|
||||
|
||||
function formatMenuItemLabel(text: string) {
|
||||
if (__WIN32__ || __LINUX__) {
|
||||
|
@ -68,6 +77,9 @@ interface INoChangesProps {
|
|||
* opening the repository in an external editor.
|
||||
*/
|
||||
readonly isExternalEditorAvailable: boolean
|
||||
|
||||
/** The user's preference of pull request suggested next action to use **/
|
||||
readonly pullRequestSuggestedNextAction?: PullRequestSuggestedNextAction
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -341,7 +353,9 @@ export class NoChanges extends React.Component<
|
|||
return this.renderPublishBranchAction(tip)
|
||||
}
|
||||
|
||||
const isForcePush = isCurrentBranchForcePush(branchesState, aheadBehind)
|
||||
const isForcePush =
|
||||
getCurrentBranchForcePushState(branchesState, aheadBehind) ===
|
||||
ForcePushBranchState.Recommended
|
||||
if (isForcePush) {
|
||||
// do not render an action currently after the rebase has completed, as
|
||||
// the default behaviour is currently to pull in changes from the tracking
|
||||
|
@ -632,12 +646,16 @@ export class NoChanges extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private renderCreatePullRequestAction(tip: IValidBranch) {
|
||||
const itemId: MenuIDs = 'create-pull-request'
|
||||
const menuItem = this.getMenuItemInfo(itemId)
|
||||
private onPullRequestSuggestedActionChanged = (
|
||||
action: PullRequestSuggestedNextAction
|
||||
) => {
|
||||
this.props.dispatcher.setPullRequestSuggestedNextAction(action)
|
||||
}
|
||||
|
||||
if (menuItem === undefined) {
|
||||
log.error(`Could not find matching menu item for ${itemId}`)
|
||||
private renderCreatePullRequestAction(tip: IValidBranch) {
|
||||
const createMenuItem = this.getMenuItemInfo('create-pull-request')
|
||||
if (createMenuItem === undefined) {
|
||||
log.error(`Could not find matching menu item for 'create-pull-request'`)
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -652,17 +670,69 @@ export class NoChanges extends React.Component<
|
|||
const title = `Create a Pull Request from your current branch`
|
||||
const buttonText = `Create Pull Request`
|
||||
|
||||
if (!enableStartingPullRequests()) {
|
||||
return (
|
||||
<MenuBackedSuggestedAction
|
||||
key="create-pr-action"
|
||||
title={title}
|
||||
menuItemId={'create-pull-request'}
|
||||
description={description}
|
||||
buttonText={buttonText}
|
||||
discoverabilityContent={this.renderDiscoverabilityElements(
|
||||
createMenuItem
|
||||
)}
|
||||
type="primary"
|
||||
disabled={!createMenuItem.enabled}
|
||||
onClick={this.onCreatePullRequestClicked}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const previewPullMenuItem = this.getMenuItemInfo('preview-pull-request')
|
||||
|
||||
if (previewPullMenuItem === undefined) {
|
||||
log.error(`Could not find matching menu item for 'preview-pull-request'`)
|
||||
return null
|
||||
}
|
||||
|
||||
const createPullRequestAction: IDropdownSuggestedActionOption<PullRequestSuggestedNextAction> =
|
||||
{
|
||||
title,
|
||||
label: buttonText,
|
||||
description,
|
||||
value: PullRequestSuggestedNextAction.CreatePullRequest,
|
||||
menuItemId: 'create-pull-request',
|
||||
discoverabilityContent:
|
||||
this.renderDiscoverabilityElements(createMenuItem),
|
||||
disabled: !createMenuItem.enabled,
|
||||
onClick: this.onCreatePullRequestClicked,
|
||||
}
|
||||
|
||||
const previewPullRequestAction: IDropdownSuggestedActionOption<PullRequestSuggestedNextAction> =
|
||||
{
|
||||
title: `Preview the Pull Request from your current branch`,
|
||||
label: 'Preview Pull Request',
|
||||
description: (
|
||||
<>
|
||||
The current branch (<Ref>{tip.branch.name}</Ref>) is already
|
||||
published to GitHub. Preview the changes this pull request will have
|
||||
before proposing your changes.
|
||||
</>
|
||||
),
|
||||
value: PullRequestSuggestedNextAction.PreviewPullRequest,
|
||||
menuItemId: 'preview-pull-request',
|
||||
discoverabilityContent:
|
||||
this.renderDiscoverabilityElements(previewPullMenuItem),
|
||||
disabled: !previewPullMenuItem.enabled,
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuBackedSuggestedAction
|
||||
key="create-pr-action"
|
||||
title={title}
|
||||
menuItemId={itemId}
|
||||
description={description}
|
||||
buttonText={buttonText}
|
||||
discoverabilityContent={this.renderDiscoverabilityElements(menuItem)}
|
||||
type="primary"
|
||||
disabled={!menuItem.enabled}
|
||||
onClick={this.onCreatePullRequestClicked}
|
||||
<DropdownSuggestedAction
|
||||
key="pull-request-action"
|
||||
className="pull-request-action"
|
||||
suggestedActions={[previewPullRequestAction, createPullRequestAction]}
|
||||
selectedActionValue={this.props.pullRequestSuggestedNextAction}
|
||||
onSuggestedActionChanged={this.onPullRequestSuggestedActionChanged}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react'
|
|||
import { Dispatcher } from '../dispatcher'
|
||||
import { getDefaultDir, setDefaultDir } from '../lib/default-dir'
|
||||
import { Account } from '../../models/account'
|
||||
import { FoldoutType } from '../../lib/app-state'
|
||||
import {
|
||||
IRepositoryIdentifier,
|
||||
parseRepositoryIdentifier,
|
||||
|
@ -23,6 +24,7 @@ import { ClickSource } from '../lib/list'
|
|||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { showOpenDialog, showSaveDialog } from '../main-process-proxy'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { isTopMostDialog } from '../dialog/is-top-most'
|
||||
|
||||
interface ICloneRepositoryProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
|
@ -64,6 +66,9 @@ interface ICloneRepositoryProps {
|
|||
* available for cloning.
|
||||
*/
|
||||
readonly onRefreshRepositories: (account: Account) => void
|
||||
|
||||
/** Whether the dialog is the top most in the dialog stack */
|
||||
readonly isTopMost: boolean
|
||||
}
|
||||
|
||||
interface ICloneRepositoryState {
|
||||
|
@ -147,6 +152,16 @@ export class CloneRepository extends React.Component<
|
|||
ICloneRepositoryProps,
|
||||
ICloneRepositoryState
|
||||
> {
|
||||
private checkIsTopMostDialog = isTopMostDialog(
|
||||
() => {
|
||||
this.validatePath()
|
||||
window.addEventListener('focus', this.onWindowFocus)
|
||||
},
|
||||
() => {
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
}
|
||||
)
|
||||
|
||||
public constructor(props: ICloneRepositoryProps) {
|
||||
super(props)
|
||||
|
||||
|
@ -191,6 +206,8 @@ export class CloneRepository extends React.Component<
|
|||
if (prevProps.initialURL !== this.props.initialURL) {
|
||||
this.updateUrl(this.props.initialURL || '')
|
||||
}
|
||||
|
||||
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -199,7 +216,11 @@ export class CloneRepository extends React.Component<
|
|||
this.updateUrl(initialURL)
|
||||
}
|
||||
|
||||
window.addEventListener('focus', this.onWindowFocus)
|
||||
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.checkIsTopMostDialog(false)
|
||||
}
|
||||
|
||||
private initializePath = async () => {
|
||||
|
@ -223,10 +244,6 @@ export class CloneRepository extends React.Component<
|
|||
this.updateUrl(selectedTabState.url)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { error } = this.getSelectedTabState()
|
||||
return (
|
||||
|
@ -728,6 +745,7 @@ export class CloneRepository extends React.Component<
|
|||
|
||||
const { url, defaultBranch } = cloneInfo
|
||||
|
||||
this.props.dispatcher.closeFoldout(FoldoutType.Repository)
|
||||
try {
|
||||
this.cloneImpl(url.trim(), path, defaultBranch)
|
||||
} catch (e) {
|
||||
|
|
|
@ -3,6 +3,32 @@ import classNames from 'classnames'
|
|||
import { DialogHeader } from './header'
|
||||
import { createUniqueId, releaseUniqueId } from '../lib/id-pool'
|
||||
import { getTitleBarHeight } from '../window/title-bar'
|
||||
import { isTopMostDialog } from './is-top-most'
|
||||
|
||||
export interface IDialogStackContext {
|
||||
/** Whether or not this dialog is the top most one in the stack to be
|
||||
* interacted with by the user. This will also determine if event listeners
|
||||
* will be active or not. */
|
||||
isTopMost: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The DialogStackContext is used to communicate between the `Dialog` and the
|
||||
* `App` information that is mostly unique to the `Dialog` component such as
|
||||
* whether it is at the top of the popup stack. Some, but not the vast majority,
|
||||
* custom popup components in between may also utilize this to enable and
|
||||
* disable event listeners in response to changes in whether it is the top most
|
||||
* popup.
|
||||
*
|
||||
* NB *** React.Context is not the preferred method of passing data to child
|
||||
* components for this code base. We are choosing to use it here as implementing
|
||||
* prop drilling would be extremely tedious and would lead to adding `Dialog`
|
||||
* props on 60+ components that would not otherwise use them. ***
|
||||
*
|
||||
*/
|
||||
export const DialogStackContext = React.createContext<IDialogStackContext>({
|
||||
isTopMost: false,
|
||||
})
|
||||
|
||||
/**
|
||||
* The time (in milliseconds) from when the dialog is mounted
|
||||
|
@ -138,6 +164,18 @@ interface IDialogState {
|
|||
* out of the dialog without first dismissing it.
|
||||
*/
|
||||
export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
||||
public static contextType = DialogStackContext
|
||||
public declare context: React.ContextType<typeof DialogStackContext>
|
||||
|
||||
private checkIsTopMostDialog = isTopMostDialog(
|
||||
() => {
|
||||
this.onDialogIsTopMost()
|
||||
},
|
||||
() => {
|
||||
this.onDialogIsNotTopMost()
|
||||
}
|
||||
)
|
||||
|
||||
private dialogElement: HTMLDialogElement | null = null
|
||||
private dismissGraceTimeoutId?: number
|
||||
|
||||
|
@ -214,6 +252,13 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
|||
|
||||
private onDismissGraceTimer = () => {
|
||||
this.setState({ isAppearing: false })
|
||||
|
||||
this.dialogElement?.dispatchEvent(
|
||||
new CustomEvent('dialog-appeared', {
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private isDismissable() {
|
||||
|
@ -242,11 +287,17 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!this.dialogElement) {
|
||||
this.checkIsTopMostDialog(this.context.isTopMost)
|
||||
}
|
||||
|
||||
protected onDialogIsTopMost() {
|
||||
if (this.dialogElement == null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.dialogElement.showModal()
|
||||
if (!this.dialogElement.open) {
|
||||
this.dialogElement.showModal()
|
||||
}
|
||||
|
||||
// Provide an event that components can subscribe to in order to perform
|
||||
// tasks such as re-layout after the dialog is visible
|
||||
|
@ -268,6 +319,20 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
|||
window.addEventListener('resize', this.scheduleResizeEvent)
|
||||
}
|
||||
|
||||
protected onDialogIsNotTopMost() {
|
||||
if (this.dialogElement !== null && this.dialogElement.open) {
|
||||
this.dialogElement?.close()
|
||||
}
|
||||
|
||||
this.clearDismissGraceTimeout()
|
||||
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
document.removeEventListener('mouseup', this.onDocumentMouseUp)
|
||||
|
||||
this.resizeObserver.disconnect()
|
||||
window.removeEventListener('resize', this.scheduleResizeEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to move keyboard focus to the first _suitable_ child of the
|
||||
* dialog.
|
||||
|
@ -418,23 +483,19 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
|||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.clearDismissGraceTimeout()
|
||||
|
||||
if (this.state.titleId) {
|
||||
releaseUniqueId(this.state.titleId)
|
||||
}
|
||||
|
||||
window.removeEventListener('focus', this.onWindowFocus)
|
||||
document.removeEventListener('mouseup', this.onDocumentMouseUp)
|
||||
|
||||
this.resizeObserver.disconnect()
|
||||
window.removeEventListener('resize', this.scheduleResizeEvent)
|
||||
this.checkIsTopMostDialog(false)
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
public componentDidUpdate(prevProps: IDialogProps) {
|
||||
if (!this.props.title && this.state.titleId) {
|
||||
this.updateTitleId()
|
||||
}
|
||||
|
||||
this.checkIsTopMostDialog(this.context.isTopMost)
|
||||
}
|
||||
|
||||
private onDialogCancel = (e: Event | React.SyntheticEvent) => {
|
||||
|
|
17
app/src/ui/dialog/is-top-most.tsx
Normal file
17
app/src/ui/dialog/is-top-most.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import memoizeOne from 'memoize-one'
|
||||
|
||||
/** This method is a memoizedOne for a consistent means of handling when the
|
||||
* isTopMost property of the `DialogStackContext` changes in the various popups
|
||||
* that consume it. */
|
||||
export function isTopMostDialog(
|
||||
onDialogIsTopMost: () => void,
|
||||
onDialogIsNotTopMost: () => void
|
||||
) {
|
||||
return memoizeOne((isTopMost: boolean) => {
|
||||
if (isTopMost) {
|
||||
onDialogIsTopMost()
|
||||
} else {
|
||||
onDialogIsNotTopMost()
|
||||
}
|
||||
})
|
||||
}
|
|
@ -164,6 +164,14 @@ export class CodeMirrorHost extends React.Component<ICodeMirrorHostProps, {}> {
|
|||
|
||||
CodeMirrorHost.updateDoc(this.codeMirror, this.props.value)
|
||||
this.resizeObserver.observe(this.codeMirror.getWrapperElement())
|
||||
|
||||
if (this.wrapper !== null && this.wrapper.closest('dialog') !== null) {
|
||||
document.addEventListener('dialog-appeared', this.onDialogAppeared)
|
||||
}
|
||||
}
|
||||
|
||||
private onDialogAppeared = () => {
|
||||
requestAnimationFrame(this.onResized)
|
||||
}
|
||||
|
||||
private onSwapDoc = (cm: Editor, oldDoc: Doc) => {
|
||||
|
@ -199,6 +207,7 @@ export class CodeMirrorHost extends React.Component<ICodeMirrorHostProps, {}> {
|
|||
}
|
||||
|
||||
this.resizeObserver.disconnect()
|
||||
document.removeEventListener('dialog-show', this.onDialogAppeared)
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: ICodeMirrorHostProps) {
|
||||
|
|
|
@ -4,14 +4,13 @@ import { Octicon } from '../octicons'
|
|||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { RadioButton } from '../lib/radio-button'
|
||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||
import { RepositorySectionTab } from '../../lib/app-state'
|
||||
|
||||
interface IDiffOptionsProps {
|
||||
readonly sourceTab: RepositorySectionTab
|
||||
readonly isInteractiveDiff: boolean
|
||||
readonly hideWhitespaceChanges: boolean
|
||||
readonly onHideWhitespaceChangesChanged: (
|
||||
hideWhitespaceChanges: boolean
|
||||
) => Promise<void>
|
||||
) => void
|
||||
|
||||
readonly showSideBySideDiff: boolean
|
||||
readonly onShowSideBySideDiffChanged: (showSideBySideDiff: boolean) => void
|
||||
|
@ -144,7 +143,7 @@ export class DiffOptions extends React.Component<
|
|||
__DARWIN__ ? 'Hide Whitespace Changes' : 'Hide whitespace changes'
|
||||
}
|
||||
/>
|
||||
{this.props.sourceTab === RepositorySectionTab.Changes && (
|
||||
{this.props.isInteractiveDiff && (
|
||||
<p className="secondary-text">
|
||||
Interacting with individual lines or hunks will be disabled while
|
||||
hiding whitespace.
|
||||
|
|
|
@ -363,6 +363,16 @@ export class SideBySideDiffRow extends React.Component<
|
|||
throw new Error(`Unexpected expansion type ${expansionType}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the width of a line gutter in pixels. For unified diffs
|
||||
* the gutter contains the line number of both before and after sides, whereas
|
||||
* for side-by-side diffs the gutter contains the line number of only one side.
|
||||
*/
|
||||
private get lineGutterWidth() {
|
||||
const { showSideBySideDiff, lineNumberWidth } = this.props
|
||||
return showSideBySideDiff ? lineNumberWidth : lineNumberWidth * 2
|
||||
}
|
||||
|
||||
private renderHunkExpansionHandle(
|
||||
hunkIndex: number,
|
||||
expansionType: DiffHunkExpansionType
|
||||
|
@ -372,7 +382,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
<div
|
||||
className="hunk-expansion-handle"
|
||||
onContextMenu={this.props.onContextMenuExpandHunk}
|
||||
style={{ width: this.props.lineNumberWidth }}
|
||||
style={{ width: this.lineGutterWidth }}
|
||||
>
|
||||
<span></span>
|
||||
</div>
|
||||
|
@ -389,7 +399,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
<div
|
||||
className="hunk-expansion-handle selectable hoverable"
|
||||
onClick={elementInfo.handler}
|
||||
style={{ width: this.props.lineNumberWidth }}
|
||||
style={{ width: this.lineGutterWidth }}
|
||||
onContextMenu={this.props.onContextMenuExpandHunk}
|
||||
>
|
||||
<TooltippedContent
|
||||
|
@ -426,6 +436,12 @@ export class SideBySideDiffRow extends React.Component<
|
|||
return null
|
||||
}
|
||||
|
||||
// In unified mode, the hunk handle left position depends on the line gutter
|
||||
// width.
|
||||
const style: React.CSSProperties = this.props.showSideBySideDiff
|
||||
? {}
|
||||
: { left: this.lineGutterWidth }
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
|
@ -434,6 +450,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
onMouseLeave={this.onMouseLeaveHunk}
|
||||
onClick={this.onClickHunk}
|
||||
onContextMenu={this.onContextMenuHunk}
|
||||
style={style}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
|
@ -452,10 +469,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
) {
|
||||
if (!this.props.isDiffSelectable || isSelected === undefined) {
|
||||
return (
|
||||
<div
|
||||
className="line-number"
|
||||
style={{ width: this.props.lineNumberWidth }}
|
||||
>
|
||||
<div className="line-number" style={{ width: this.lineGutterWidth }}>
|
||||
{lineNumbers.map((lineNumber, index) => (
|
||||
<span key={index}>{lineNumber}</span>
|
||||
))}
|
||||
|
@ -470,7 +484,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
'line-selected': isSelected,
|
||||
hover: this.props.isHunkHovered,
|
||||
})}
|
||||
style={{ width: this.props.lineNumberWidth }}
|
||||
style={{ width: this.lineGutterWidth }}
|
||||
onMouseDown={this.onMouseDownLineNumber}
|
||||
onContextMenu={this.onContextMenuLineNumber}
|
||||
>
|
||||
|
@ -493,7 +507,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
|
||||
const style: React.CSSProperties = {
|
||||
[column === DiffColumn.Before ? 'marginRight' : 'marginLeft']:
|
||||
this.props.lineNumberWidth + 10,
|
||||
this.lineGutterWidth + 10,
|
||||
marginTop: -10,
|
||||
}
|
||||
|
||||
|
|
|
@ -256,7 +256,7 @@ export class SideBySideDiff extends React.Component<
|
|||
: [DiffLineType.Add, DiffLineType.Context]
|
||||
: [DiffLineType.Add, DiffLineType.Delete, DiffLineType.Context]
|
||||
|
||||
const contents = this.props.diff.hunks
|
||||
const contents = this.state.diff.hunks
|
||||
.flatMap(h =>
|
||||
h.lines
|
||||
.filter(line => lineTypes.includes(line.type))
|
||||
|
|
|
@ -68,7 +68,10 @@ import { FetchType } from '../../models/fetch'
|
|||
import { GitHubRepository } from '../../models/github-repository'
|
||||
import { ManualConflictResolution } from '../../models/manual-conflict-resolution'
|
||||
import { Popup, PopupType } from '../../models/popup'
|
||||
import { PullRequest } from '../../models/pull-request'
|
||||
import {
|
||||
PullRequest,
|
||||
PullRequestSuggestedNextAction,
|
||||
} from '../../models/pull-request'
|
||||
import {
|
||||
Repository,
|
||||
RepositoryWithGitHubRepository,
|
||||
|
@ -384,6 +387,13 @@ export class Dispatcher {
|
|||
return this.appStore._closePopup(popupType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the popup with given id.
|
||||
*/
|
||||
public closePopupById(popupId: string) {
|
||||
return this.appStore._closePopupById(popupId)
|
||||
}
|
||||
|
||||
/** Show the foldout. This will close any current popup. */
|
||||
public showFoldout(foldout: Foldout): Promise<void> {
|
||||
return this.appStore._showFoldout(foldout)
|
||||
|
@ -765,11 +775,6 @@ export class Dispatcher {
|
|||
return this.appStore._pushError(error)
|
||||
}
|
||||
|
||||
/** Clear the given error. */
|
||||
public clearError(error: Error): Promise<void> {
|
||||
return this.appStore._clearError(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a missing repository to the previous path, and update it's
|
||||
* state in the repository list if the clone completes without error.
|
||||
|
@ -2122,6 +2127,19 @@ export class Dispatcher {
|
|||
)
|
||||
}
|
||||
|
||||
/** Change the hide whitespace in pull request diff setting */
|
||||
public onHideWhitespaceInPullRequestDiffChanged(
|
||||
hideWhitespaceInDiff: boolean,
|
||||
repository: Repository,
|
||||
file: CommittedFileChange | null = null
|
||||
) {
|
||||
this.appStore._setHideWhitespaceInPullRequestDiff(
|
||||
hideWhitespaceInDiff,
|
||||
repository,
|
||||
file
|
||||
)
|
||||
}
|
||||
|
||||
/** Change the side by side diff setting */
|
||||
public onShowSideBySideDiffChanged(showSideBySideDiff: boolean) {
|
||||
return this.appStore._setShowSideBySideDiff(showSideBySideDiff)
|
||||
|
@ -2175,8 +2193,11 @@ export class Dispatcher {
|
|||
* openCreatePullRequestInBrowser method which immediately opens the
|
||||
* create pull request page without showing a dialog.
|
||||
*/
|
||||
public createPullRequest(repository: Repository): Promise<void> {
|
||||
return this.appStore._createPullRequest(repository)
|
||||
public createPullRequest(
|
||||
repository: Repository,
|
||||
baseBranch?: Branch
|
||||
): Promise<void> {
|
||||
return this.appStore._createPullRequest(repository, baseBranch)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2338,6 +2359,10 @@ export class Dispatcher {
|
|||
await this.appStore._loadStatus(repository)
|
||||
}
|
||||
|
||||
public setConfirmDiscardStashSetting(value: boolean) {
|
||||
return this.appStore._setConfirmDiscardStashSetting(value)
|
||||
}
|
||||
|
||||
public setConfirmForcePushSetting(value: boolean) {
|
||||
return this.appStore._setConfirmForcePushSetting(value)
|
||||
}
|
||||
|
@ -2435,6 +2460,10 @@ export class Dispatcher {
|
|||
return this.statsStore.recordCreatePullRequest()
|
||||
}
|
||||
|
||||
public recordCreatePullRequestFromPreview() {
|
||||
return this.statsStore.recordCreatePullRequestFromPreview()
|
||||
}
|
||||
|
||||
public recordWelcomeWizardInitiated() {
|
||||
return this.statsStore.recordWelcomeWizardInitiated()
|
||||
}
|
||||
|
@ -3963,9 +3992,63 @@ export class Dispatcher {
|
|||
|
||||
public startPullRequest(repository: Repository) {
|
||||
this.appStore._startPullRequest(repository)
|
||||
}
|
||||
|
||||
this.showPopup({
|
||||
type: PopupType.StartPullRequest,
|
||||
})
|
||||
/**
|
||||
* Change the selected changed file of the current pull request state.
|
||||
*/
|
||||
public changePullRequestFileSelection(
|
||||
repository: Repository,
|
||||
file: CommittedFileChange
|
||||
): Promise<void> {
|
||||
return this.appStore._changePullRequestFileSelection(repository, file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width of the file list column in the pull request files changed
|
||||
*/
|
||||
public setPullRequestFileListWidth(width: number): Promise<void> {
|
||||
return this.appStore._setPullRequestFileListWidth(width)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the width of the file list column in the pull request files changed
|
||||
*/
|
||||
public resetPullRequestFileListWidth(): Promise<void> {
|
||||
return this.appStore._resetPullRequestFileListWidth()
|
||||
}
|
||||
|
||||
public updatePullRequestBaseBranch(repository: Repository, branch: Branch) {
|
||||
this.appStore._updatePullRequestBaseBranch(repository, branch)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to quit the app if it's not updating, unless requested to quit
|
||||
* even if it is updating.
|
||||
*
|
||||
* @param evenIfUpdating Whether to quit even if the app is updating.
|
||||
*/
|
||||
public quitApp(evenIfUpdating: boolean) {
|
||||
this.appStore._quitApp(evenIfUpdating)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels quitting the app. This could be needed if, on macOS, the user tries
|
||||
* to quit the app while an update is in progress, but then after being
|
||||
* informed about the issues that could cause they decided to not close the
|
||||
* app yet.
|
||||
*/
|
||||
public cancelQuittingApp() {
|
||||
this.appStore._cancelQuittingApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's preference for which pull request suggested next action to
|
||||
* use
|
||||
*/
|
||||
public setPullRequestSuggestedNextAction(
|
||||
value: PullRequestSuggestedNextAction
|
||||
) {
|
||||
return this.appStore._setPullRequestSuggestedNextAction(value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Button } from './lib/button'
|
|||
import { Octicon } from './octicons'
|
||||
import * as OcticonSymbol from './octicons/octicons.generated'
|
||||
|
||||
export interface IDropdownSelectButtonOption {
|
||||
export interface IDropdownSelectButtonOption<T extends string> {
|
||||
/** The select option header label. */
|
||||
readonly label?: string | JSX.Element
|
||||
|
||||
|
@ -12,15 +12,15 @@ export interface IDropdownSelectButtonOption {
|
|||
readonly description?: string | JSX.Element
|
||||
|
||||
/** The select option's value */
|
||||
readonly value?: string
|
||||
readonly value: T
|
||||
}
|
||||
|
||||
interface IDropdownSelectButtonProps {
|
||||
interface IDropdownSelectButtonProps<T extends string> {
|
||||
/** The selection button options */
|
||||
readonly options: ReadonlyArray<IDropdownSelectButtonOption>
|
||||
readonly options: ReadonlyArray<IDropdownSelectButtonOption<T>>
|
||||
|
||||
/** The selection option value */
|
||||
readonly selectedValue?: string
|
||||
readonly selectedValue?: T
|
||||
|
||||
/** Whether or not the button is enabled */
|
||||
readonly disabled?: boolean
|
||||
|
@ -30,22 +30,22 @@ interface IDropdownSelectButtonProps {
|
|||
|
||||
/** Callback for when the button selection changes*/
|
||||
readonly onSelectChange?: (
|
||||
selectedOption: IDropdownSelectButtonOption
|
||||
selectedOption: IDropdownSelectButtonOption<T>
|
||||
) => void
|
||||
|
||||
/** Callback for when button is selected option button is clicked */
|
||||
readonly onSubmit?: (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
selectedOption: IDropdownSelectButtonOption
|
||||
selectedOption: IDropdownSelectButtonOption<T>
|
||||
) => void
|
||||
}
|
||||
|
||||
interface IDropdownSelectButtonState {
|
||||
interface IDropdownSelectButtonState<T extends string> {
|
||||
/** Whether the options are rendered */
|
||||
readonly showButtonOptions: boolean
|
||||
|
||||
/** The currently selected option */
|
||||
readonly selectedOption: IDropdownSelectButtonOption | null
|
||||
readonly selectedOption: IDropdownSelectButtonOption<T> | null
|
||||
|
||||
/**
|
||||
* The adjusting position of the options popover. This is calculated based
|
||||
|
@ -54,14 +54,16 @@ interface IDropdownSelectButtonState {
|
|||
readonly optionsPositionBottom?: string
|
||||
}
|
||||
|
||||
export class DropdownSelectButton extends React.Component<
|
||||
IDropdownSelectButtonProps,
|
||||
IDropdownSelectButtonState
|
||||
export class DropdownSelectButton<
|
||||
T extends string = string
|
||||
> extends React.Component<
|
||||
IDropdownSelectButtonProps<T>,
|
||||
IDropdownSelectButtonState<T>
|
||||
> {
|
||||
private invokeButtonRef: HTMLButtonElement | null = null
|
||||
private optionsContainerRef: HTMLDivElement | null = null
|
||||
|
||||
public constructor(props: IDropdownSelectButtonProps) {
|
||||
public constructor(props: IDropdownSelectButtonProps<T>) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
|
@ -89,9 +91,64 @@ export class DropdownSelectButton extends React.Component<
|
|||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener('keydown', this.onKeyDown)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.onKeyDown)
|
||||
}
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent) => {
|
||||
const { key } = event
|
||||
if (this.state.showButtonOptions && key === 'Escape') {
|
||||
this.setState({ showButtonOptions: false })
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!this.state.showButtonOptions ||
|
||||
!['ArrowUp', 'ArrowDown'].includes(key)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const buttons = this.optionsContainerRef?.querySelectorAll(
|
||||
'.dropdown-select-button-options .button-component'
|
||||
)
|
||||
|
||||
if (buttons === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const foundCurrentIndex = [...buttons].findIndex(b =>
|
||||
b.className.includes('focus')
|
||||
)
|
||||
|
||||
let focusedOptionIndex = -1
|
||||
if (foundCurrentIndex !== -1) {
|
||||
if (key === 'ArrowUp') {
|
||||
focusedOptionIndex =
|
||||
foundCurrentIndex !== 0
|
||||
? foundCurrentIndex - 1
|
||||
: this.props.options.length - 1
|
||||
} else {
|
||||
focusedOptionIndex =
|
||||
foundCurrentIndex !== this.props.options.length - 1
|
||||
? foundCurrentIndex + 1
|
||||
: 0
|
||||
}
|
||||
} else {
|
||||
focusedOptionIndex = key === 'ArrowUp' ? this.props.options.length - 1 : 0
|
||||
}
|
||||
|
||||
const button = buttons?.item(focusedOptionIndex) as HTMLButtonElement
|
||||
button?.focus()
|
||||
}
|
||||
|
||||
private getSelectedOption(
|
||||
selectedValue: string | undefined
|
||||
): IDropdownSelectButtonOption | null {
|
||||
selectedValue: T | undefined
|
||||
): IDropdownSelectButtonOption<T> | null {
|
||||
const { options } = this.props
|
||||
if (options.length === 0) {
|
||||
return null
|
||||
|
@ -104,8 +161,10 @@ export class DropdownSelectButton extends React.Component<
|
|||
return selectedOption
|
||||
}
|
||||
|
||||
private onSelectionChange = (selectedOption: IDropdownSelectButtonOption) => {
|
||||
return (_event: React.MouseEvent<HTMLLIElement, MouseEvent>) => {
|
||||
private onSelectionChange = (
|
||||
selectedOption: IDropdownSelectButtonOption<T>
|
||||
) => {
|
||||
return (_event?: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
this.setState({ selectedOption, showButtonOptions: false })
|
||||
|
||||
const { onSelectChange } = this.props
|
||||
|
@ -127,7 +186,7 @@ export class DropdownSelectButton extends React.Component<
|
|||
this.optionsContainerRef = ref
|
||||
}
|
||||
|
||||
private renderSelectedIcon(option: IDropdownSelectButtonOption) {
|
||||
private renderSelectedIcon(option: IDropdownSelectButtonOption<T>) {
|
||||
const { selectedOption } = this.state
|
||||
if (selectedOption === null || option.value !== selectedOption.value) {
|
||||
return
|
||||
|
@ -141,6 +200,16 @@ export class DropdownSelectButton extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private renderOption = (o: IDropdownSelectButtonOption<T>) => {
|
||||
return (
|
||||
<Button key={o.value} onClick={this.onSelectionChange(o)}>
|
||||
{this.renderSelectedIcon(o)}
|
||||
<div className="option-title">{o.label}</div>
|
||||
<div className="option-description">{o.description}</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
private renderSplitButtonOptions() {
|
||||
if (!this.state.showButtonOptions) {
|
||||
return
|
||||
|
@ -150,22 +219,14 @@ export class DropdownSelectButton extends React.Component<
|
|||
const { optionsPositionBottom: bottom } = this.state
|
||||
const openClass = bottom !== undefined ? 'open-top' : 'open-bottom'
|
||||
const classes = classNames('dropdown-select-button-options', openClass)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
style={{ bottom }}
|
||||
ref={this.onOptionsContainerRef}
|
||||
>
|
||||
<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>
|
||||
<div className="option-description">{o.description}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{options.map(o => this.renderOption(o))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -199,23 +260,25 @@ export class DropdownSelectButton extends React.Component<
|
|||
// method.
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<Button
|
||||
className="invoke-button"
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
tooltip={this.props.tooltip}
|
||||
onButtonRef={this.onInvokeButtonRef}
|
||||
onClick={this.onSubmit}
|
||||
>
|
||||
{selectedOption.label}
|
||||
</Button>
|
||||
<Button
|
||||
className={dropdownClasses}
|
||||
onClick={this.openSplitButtonDropdown}
|
||||
type="button"
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
||||
</Button>
|
||||
<div className="dropdown-button-wrappers">
|
||||
<Button
|
||||
className="invoke-button"
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
tooltip={this.props.tooltip}
|
||||
onButtonRef={this.onInvokeButtonRef}
|
||||
onClick={this.onSubmit}
|
||||
>
|
||||
{selectedOption.label}
|
||||
</Button>
|
||||
<Button
|
||||
className={dropdownClasses}
|
||||
onClick={this.openSplitButtonDropdown}
|
||||
type="button"
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
||||
</Button>
|
||||
</div>
|
||||
{this.renderSplitButtonOptions()}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -219,6 +219,10 @@ export class CommitListItem extends React.PureComponent<
|
|||
clipboard.writeText(this.props.commit.sha)
|
||||
}
|
||||
|
||||
private onCopyTags = () => {
|
||||
clipboard.writeText(this.props.commit.tags.join(' '))
|
||||
}
|
||||
|
||||
private onViewOnGitHub = () => {
|
||||
if (this.props.onViewCommitOnGitHub) {
|
||||
this.props.onViewCommitOnGitHub(this.props.commit.sha)
|
||||
|
@ -341,7 +345,10 @@ export class CommitListItem extends React.PureComponent<
|
|||
deleteTagsMenuItem
|
||||
)
|
||||
}
|
||||
|
||||
const darwinTagsLabel =
|
||||
this.props.commit.tags.length > 1 ? 'Copy Tags' : 'Copy Tag'
|
||||
const windowTagsLabel =
|
||||
this.props.commit.tags.length > 1 ? 'Copy tags' : 'Copy tag'
|
||||
items.push(
|
||||
{
|
||||
label: __DARWIN__ ? 'Cherry-pick Commit…' : 'Cherry-pick commit…',
|
||||
|
@ -353,6 +360,11 @@ export class CommitListItem extends React.PureComponent<
|
|||
label: 'Copy SHA',
|
||||
action: this.onCopySHA,
|
||||
},
|
||||
{
|
||||
label: __DARWIN__ ? darwinTagsLabel : windowTagsLabel,
|
||||
action: this.onCopyTags,
|
||||
enabled: this.props.commit.tags.length > 0,
|
||||
},
|
||||
{
|
||||
label: viewOnGitHubLabel,
|
||||
action: this.onViewOnGitHub,
|
||||
|
|
|
@ -12,7 +12,6 @@ import { CommitAttribution } from '../lib/commit-attribution'
|
|||
import { Tokenizer, TokenResult } from '../../lib/text-token-parser'
|
||||
import { wrapRichTextCommitMessage } from '../../lib/wrap-rich-text-commit-message'
|
||||
import { DiffOptions } from '../diff/diff-options'
|
||||
import { RepositorySectionTab } from '../../lib/app-state'
|
||||
import { IChangesetData } from '../../lib/git'
|
||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||
import { AppFileStatusKind } from '../../models/status'
|
||||
|
@ -431,7 +430,10 @@ export class CommitSummary extends React.Component<
|
|||
aria-label="SHA"
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.gitCommit} />
|
||||
<TooltippedCommitSHA className="sha" commit={selectedCommits[0]} />
|
||||
<TooltippedCommitSHA
|
||||
className="selectable"
|
||||
commit={selectedCommits[0]}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
@ -505,7 +507,7 @@ export class CommitSummary extends React.Component<
|
|||
title="Diff Options"
|
||||
>
|
||||
<DiffOptions
|
||||
sourceTab={RepositorySectionTab.History}
|
||||
isInteractiveDiff={false}
|
||||
hideWhitespaceChanges={this.props.hideWhitespaceInDiff}
|
||||
onHideWhitespaceChangesChanged={
|
||||
this.props.onHideWhitespaceInDiffChanged
|
||||
|
@ -642,7 +644,7 @@ export class CommitSummary extends React.Component<
|
|||
<Octicon symbol={OcticonSymbol.tag} />
|
||||
</span>
|
||||
|
||||
<span className="tags">{tags.join(', ')}</span>
|
||||
<span className="tags selectable">{tags.join(', ')}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -96,25 +96,24 @@ export class MergeCallToActionWithConflicts extends React.Component<
|
|||
})
|
||||
}
|
||||
|
||||
private onOperationChange = (option: IDropdownSelectButtonOption) => {
|
||||
const value = option.value as MultiCommitOperationKind
|
||||
this.setState({ selectedOperation: value })
|
||||
if (value === MultiCommitOperationKind.Rebase) {
|
||||
private onOperationChange = (
|
||||
option: IDropdownSelectButtonOption<MultiCommitOperationKind>
|
||||
) => {
|
||||
this.setState({ selectedOperation: option.value })
|
||||
if (option.value === MultiCommitOperationKind.Rebase) {
|
||||
this.updateRebasePreview(this.props.comparisonBranch)
|
||||
}
|
||||
}
|
||||
|
||||
private onOperationInvoked = async (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
selectedOption: IDropdownSelectButtonOption
|
||||
selectedOption: IDropdownSelectButtonOption<MultiCommitOperationKind>
|
||||
) => {
|
||||
event.preventDefault()
|
||||
|
||||
const { dispatcher, repository } = this.props
|
||||
|
||||
await this.dispatchOperation(
|
||||
selectedOption.value as MultiCommitOperationKind
|
||||
)
|
||||
await this.dispatchOperation(selectedOption.value)
|
||||
|
||||
dispatcher.executeCompare(repository, {
|
||||
kind: HistoryTabMode.History,
|
||||
|
|
|
@ -168,8 +168,8 @@ const sendErrorWithContext = (
|
|||
extra.windowZoomFactor = `${currentState.windowZoomFactor}`
|
||||
}
|
||||
|
||||
if (currentState.errors.length > 0) {
|
||||
extra.activeAppErrors = `${currentState.errors.length}`
|
||||
if (currentState.errorCount > 0) {
|
||||
extra.activeAppErrors = `${currentState.errorCount}`
|
||||
}
|
||||
|
||||
extra.repositoryCount = `${currentState.repositories.length}`
|
||||
|
|
96
app/src/ui/installing-update/installing-update.tsx
Normal file
96
app/src/ui/installing-update/installing-update.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { Row } from '../lib/row'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
OkCancelButtonGroup,
|
||||
DialogFooter,
|
||||
} from '../dialog'
|
||||
import { updateStore, IUpdateState, UpdateStatus } from '../lib/update-store'
|
||||
import { Disposable } from 'event-kit'
|
||||
import { DialogHeader } from '../dialog/header'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
|
||||
interface IInstallingUpdateProps {
|
||||
/**
|
||||
* Event triggered when the dialog is dismissed by the user in the
|
||||
* ways described in the Dialog component's dismissable prop.
|
||||
*/
|
||||
readonly onDismissed: () => void
|
||||
|
||||
readonly dispatcher: Dispatcher
|
||||
}
|
||||
|
||||
/**
|
||||
* A dialog that presents information about the
|
||||
* running application such as name and version.
|
||||
*/
|
||||
export class InstallingUpdate extends React.Component<IInstallingUpdateProps> {
|
||||
private updateStoreEventHandle: Disposable | null = null
|
||||
|
||||
private onUpdateStateChanged = (updateState: IUpdateState) => {
|
||||
// If the update is not being downloaded (`UpdateStatus.UpdateAvailable`),
|
||||
// i.e. if it's already downloaded or not available, close the window.
|
||||
if (updateState.status !== UpdateStatus.UpdateAvailable) {
|
||||
this.props.dispatcher.quitApp(false)
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.updateStoreEventHandle = updateStore.onDidChange(
|
||||
this.onUpdateStateChanged
|
||||
)
|
||||
|
||||
// Manually update the state to ensure we're in sync with the store
|
||||
this.onUpdateStateChanged(updateStore.state)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.updateStoreEventHandle) {
|
||||
this.updateStoreEventHandle.dispose()
|
||||
this.updateStoreEventHandle = null
|
||||
}
|
||||
|
||||
// This will ensure the app doesn't try to quit after the update is
|
||||
// installed once the dialog is closed (explicitly or implicitly, by
|
||||
// opening another dialog on top of this one).
|
||||
this.props.dispatcher.cancelQuittingApp()
|
||||
}
|
||||
|
||||
private onQuitAnywayButtonClicked = () => {
|
||||
this.props.dispatcher.quitApp(true)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Dialog
|
||||
id="installing-update"
|
||||
onSubmit={this.props.onDismissed}
|
||||
dismissable={false}
|
||||
type="warning"
|
||||
>
|
||||
<DialogHeader
|
||||
title={__DARWIN__ ? 'Installing Update…' : 'Installing update…'}
|
||||
loading={true}
|
||||
dismissable={true}
|
||||
onDismissed={this.props.onDismissed}
|
||||
/>
|
||||
<DialogContent>
|
||||
<Row className="updating-message">
|
||||
Do not close GitHub Desktop while the update is in progress. Closing
|
||||
now may break your installation.
|
||||
</Row>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<OkCancelButtonGroup
|
||||
okButtonText={__DARWIN__ ? 'Quit Anyway' : 'Quit anyway'}
|
||||
onOkButtonClick={this.onQuitAnywayButtonClicked}
|
||||
onCancelButtonClick={this.props.onDismissed}
|
||||
destructive={true}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -51,10 +51,28 @@ export class Draggable extends React.Component<IDraggableProps> {
|
|||
this.dragElement = document.getElementById('dragElement')
|
||||
}
|
||||
|
||||
/**
|
||||
* A user can drag a commit if they are holding down the left mouse button or
|
||||
* event.button === 0
|
||||
*
|
||||
* Exceptions:
|
||||
* - macOS allow emulating a right click by holding down the ctrl and left
|
||||
* mouse button.
|
||||
* - user can not drag during a shift click
|
||||
*
|
||||
* All other MouseEvent.button values are:
|
||||
* 2: right button/pen barrel button
|
||||
* 1: middle button
|
||||
* X1, X2: mouse back/forward buttons
|
||||
* 5: pen eraser
|
||||
* -1: No button changed
|
||||
*
|
||||
* Ref: https://www.w3.org/TR/pointerevents/#the-button-property
|
||||
*
|
||||
* */
|
||||
private canDragCommit(event: React.MouseEvent<HTMLDivElement>): boolean {
|
||||
// right clicks or shift clicks
|
||||
const isSpecialClick =
|
||||
event.button === 2 ||
|
||||
event.button !== 0 ||
|
||||
(__DARWIN__ && event.button === 0 && event.ctrlKey) ||
|
||||
event.shiftKey
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ interface IListRowProps {
|
|||
readonly selected?: boolean
|
||||
|
||||
/** callback to fire when the DOM element is created */
|
||||
readonly onRef?: (element: HTMLDivElement | null) => void
|
||||
readonly onRowRef?: (index: number, element: HTMLDivElement | null) => void
|
||||
|
||||
/** callback to fire when the row receives a mouseover event */
|
||||
readonly onRowMouseOver: (index: number, e: React.MouseEvent<any>) => void
|
||||
|
@ -41,6 +41,18 @@ interface IListRowProps {
|
|||
/** callback to fire when the row receives a keyboard event */
|
||||
readonly onRowKeyDown: (index: number, e: React.KeyboardEvent<any>) => void
|
||||
|
||||
/** called when the row (or any of its descendants) receives focus */
|
||||
readonly onRowFocus?: (
|
||||
index: number,
|
||||
e: React.FocusEvent<HTMLDivElement>
|
||||
) => void
|
||||
|
||||
/** called when the row (and all of its descendants) loses focus */
|
||||
readonly onRowBlur?: (
|
||||
index: number,
|
||||
e: React.FocusEvent<HTMLDivElement>
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Whether or not this list row is going to be selectable either through
|
||||
* keyboard navigation, pointer clicks, or both. This is used to determine
|
||||
|
@ -53,6 +65,10 @@ interface IListRowProps {
|
|||
}
|
||||
|
||||
export class ListRow extends React.Component<IListRowProps, {}> {
|
||||
private onRef = (elem: HTMLDivElement | null) => {
|
||||
this.props.onRowRef?.(this.props.rowIndex, elem)
|
||||
}
|
||||
|
||||
private onRowMouseOver = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.props.onRowMouseOver(this.props.rowIndex, e)
|
||||
}
|
||||
|
@ -73,6 +89,14 @@ export class ListRow extends React.Component<IListRowProps, {}> {
|
|||
this.props.onRowKeyDown(this.props.rowIndex, e)
|
||||
}
|
||||
|
||||
private onFocus = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
this.props.onRowFocus?.(this.props.rowIndex, e)
|
||||
}
|
||||
|
||||
private onBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
this.props.onRowBlur?.(this.props.rowIndex, e)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const selected = this.props.selected
|
||||
const className = classNames(
|
||||
|
@ -102,13 +126,15 @@ export class ListRow extends React.Component<IListRowProps, {}> {
|
|||
role={role}
|
||||
className={className}
|
||||
tabIndex={this.props.tabIndex}
|
||||
ref={this.props.onRef}
|
||||
ref={this.onRef}
|
||||
onMouseOver={this.onRowMouseOver}
|
||||
onMouseDown={this.onRowMouseDown}
|
||||
onMouseUp={this.onRowMouseUp}
|
||||
onClick={this.onRowClick}
|
||||
onKeyDown={this.onRowKeyDown}
|
||||
style={style}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
|
|
@ -269,6 +269,8 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
private fakeScroll: HTMLDivElement | null = null
|
||||
private focusRow = -1
|
||||
|
||||
private readonly rowRefs = new Map<number, HTMLDivElement>()
|
||||
|
||||
/**
|
||||
* The style prop for our child Grid. We keep this here in order
|
||||
* to not create a new object on each render and thus forcing
|
||||
|
@ -567,6 +569,15 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onFocusWithinChanged = (focusWithin: boolean) => {
|
||||
// So the grid lost focus (we manually focus the grid if the focused list
|
||||
// item is unmounted) so we mustn't attempt to refocus the previously
|
||||
// focused list item if it scrolls back into view.
|
||||
if (!focusWithin) {
|
||||
this.focusRow = -1
|
||||
}
|
||||
}
|
||||
|
||||
private toggleSelection = (event: React.KeyboardEvent<any>) => {
|
||||
this.props.selectedRows.forEach(row => {
|
||||
if (!this.props.onRowClick) {
|
||||
|
@ -586,19 +597,26 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
})
|
||||
}
|
||||
|
||||
private onRowFocus = (index: number, e: React.FocusEvent<HTMLDivElement>) => {
|
||||
this.focusRow = index
|
||||
}
|
||||
|
||||
private onRowBlur = (index: number, e: React.FocusEvent<HTMLDivElement>) => {
|
||||
if (this.focusRow === index) {
|
||||
this.focusRow = -1
|
||||
}
|
||||
}
|
||||
|
||||
private onRowMouseOver = (row: number, event: React.MouseEvent<any>) => {
|
||||
if (this.props.selectOnHover && this.canSelectRow(row)) {
|
||||
if (
|
||||
this.props.selectedRows.includes(row) &&
|
||||
this.props.onSelectionChanged
|
||||
) {
|
||||
this.props.onSelectionChanged([row], { kind: 'hover', event })
|
||||
if (!this.props.selectedRows.includes(row)) {
|
||||
this.props.onSelectionChanged?.([row], { kind: 'hover', event })
|
||||
// By calling scrollRowToVisible we ensure that hovering over a partially
|
||||
// visible item at the top or bottom of the list scrolls it into view but
|
||||
// more importantly `scrollRowToVisible` automatically manages focus so
|
||||
// using it here allows us to piggy-back on its focus-preserving magic
|
||||
// even though we could theoretically live without scrolling
|
||||
this.scrollRowToVisible(row)
|
||||
this.scrollRowToVisible(row, this.props.focusOnHover !== false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -720,10 +738,14 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
this.scrollRowToVisible(row)
|
||||
}
|
||||
|
||||
private scrollRowToVisible(row: number) {
|
||||
private scrollRowToVisible(row: number, moveFocus = true) {
|
||||
if (this.grid !== null) {
|
||||
this.grid.scrollToCell({ rowIndex: row, columnIndex: 0 })
|
||||
this.focusRow = row
|
||||
|
||||
if (moveFocus) {
|
||||
this.focusRow = row
|
||||
this.rowRefs.get(row)?.focus({ preventScroll: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -804,12 +826,27 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onFocusedItemRef = (element: HTMLDivElement | null) => {
|
||||
if (this.props.focusOnHover !== false && element !== null) {
|
||||
element.focus()
|
||||
private onRowRef = (rowIndex: number, element: HTMLDivElement | null) => {
|
||||
if (element === null) {
|
||||
this.rowRefs.delete(rowIndex)
|
||||
} else {
|
||||
this.rowRefs.set(rowIndex, element)
|
||||
}
|
||||
|
||||
this.focusRow = -1
|
||||
if (rowIndex === this.focusRow) {
|
||||
// The currently focused row is going being unmounted so we'll move focus
|
||||
// programmatically to the grid so that keyboard navigation still works
|
||||
if (element === null) {
|
||||
const grid = ReactDOM.findDOMNode(this.grid)
|
||||
if (grid instanceof HTMLElement) {
|
||||
grid.focus({ preventScroll: true })
|
||||
}
|
||||
} else {
|
||||
// A previously focused row is being mounted again, we'll move focus
|
||||
// back to it
|
||||
element.focus({ preventScroll: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getCustomRowClassNames = (rowIndex: number) => {
|
||||
|
@ -836,17 +873,12 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
const selected = this.props.selectedRows.indexOf(rowIndex) !== -1
|
||||
const customClasses = this.getCustomRowClassNames(rowIndex)
|
||||
|
||||
const focused = rowIndex === this.focusRow
|
||||
|
||||
// An unselectable row shouldn't be focusable
|
||||
let tabIndex: number | undefined = undefined
|
||||
if (selectable) {
|
||||
tabIndex = selected && this.props.selectedRows[0] === rowIndex ? 0 : -1
|
||||
}
|
||||
|
||||
// We only need to keep a reference to the focused element
|
||||
const ref = focused ? this.onFocusedItemRef : undefined
|
||||
|
||||
const row = this.props.rowRenderer(rowIndex)
|
||||
|
||||
const element =
|
||||
|
@ -870,7 +902,7 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
<ListRow
|
||||
key={params.key}
|
||||
id={id}
|
||||
onRef={ref}
|
||||
onRowRef={this.onRowRef}
|
||||
rowCount={this.props.rowCount}
|
||||
rowIndex={rowIndex}
|
||||
selected={selected}
|
||||
|
@ -880,6 +912,8 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
onRowMouseDown={this.onRowMouseDown}
|
||||
onRowMouseUp={this.onRowMouseUp}
|
||||
onRowMouseOver={this.onRowMouseOver}
|
||||
onRowFocus={this.onRowFocus}
|
||||
onRowBlur={this.onRowBlur}
|
||||
style={params.style}
|
||||
tabIndex={tabIndex}
|
||||
children={element}
|
||||
|
@ -978,6 +1012,7 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
<FocusContainer
|
||||
className="list-focus-container"
|
||||
onKeyDown={this.onFocusContainerKeyDown}
|
||||
onFocusWithinChanged={this.onFocusWithinChanged}
|
||||
>
|
||||
<Grid
|
||||
aria-label={''}
|
||||
|
|
133
app/src/ui/lib/popover-dropdown.tsx
Normal file
133
app/src/ui/lib/popover-dropdown.tsx
Normal file
|
@ -0,0 +1,133 @@
|
|||
import * as React from 'react'
|
||||
import { Button } from './button'
|
||||
import { Popover, PopoverCaretPosition } from './popover'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const defaultPopoverContentHeight = 300
|
||||
const maxPopoverContentHeight = 500
|
||||
|
||||
interface IPopoverDropdownProps {
|
||||
readonly className?: string
|
||||
readonly contentTitle: string
|
||||
readonly buttonContent: JSX.Element | string
|
||||
readonly label: string
|
||||
}
|
||||
|
||||
interface IPopoverDropdownState {
|
||||
readonly showPopover: boolean
|
||||
readonly popoverContentHeight: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A dropdown component for displaying a dropdown button that opens
|
||||
* a popover to display contents relative to the button content.
|
||||
*/
|
||||
export class PopoverDropdown extends React.Component<
|
||||
IPopoverDropdownProps,
|
||||
IPopoverDropdownState
|
||||
> {
|
||||
private invokeButtonRef: HTMLButtonElement | null = null
|
||||
|
||||
public constructor(props: IPopoverDropdownProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
showPopover: false,
|
||||
popoverContentHeight: defaultPopoverContentHeight,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.calculateDropdownListHeight()
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.calculateDropdownListHeight()
|
||||
}
|
||||
|
||||
private calculateDropdownListHeight = () => {
|
||||
if (this.invokeButtonRef === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const windowHeight = window.innerHeight
|
||||
const bottomOfButton = this.invokeButtonRef.getBoundingClientRect().bottom
|
||||
const listHeaderHeight = 75
|
||||
const calcMaxHeight = Math.round(
|
||||
windowHeight - bottomOfButton - listHeaderHeight
|
||||
)
|
||||
|
||||
const popoverContentHeight =
|
||||
calcMaxHeight > maxPopoverContentHeight
|
||||
? maxPopoverContentHeight
|
||||
: calcMaxHeight
|
||||
if (popoverContentHeight !== this.state.popoverContentHeight) {
|
||||
this.setState({ popoverContentHeight })
|
||||
}
|
||||
}
|
||||
|
||||
private onInvokeButtonRef = (buttonRef: HTMLButtonElement | null) => {
|
||||
this.invokeButtonRef = buttonRef
|
||||
}
|
||||
|
||||
private togglePopover = () => {
|
||||
this.setState({ showPopover: !this.state.showPopover })
|
||||
}
|
||||
|
||||
public closePopover = () => {
|
||||
this.setState({ showPopover: false })
|
||||
}
|
||||
|
||||
private renderPopover() {
|
||||
if (!this.state.showPopover) {
|
||||
return
|
||||
}
|
||||
|
||||
const { contentTitle } = this.props
|
||||
const { popoverContentHeight } = this.state
|
||||
const contentStyle = { height: `${popoverContentHeight}px` }
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="popover-dropdown-popover"
|
||||
caretPosition={PopoverCaretPosition.TopLeft}
|
||||
onClickOutside={this.closePopover}
|
||||
>
|
||||
<div className="popover-dropdown-header">
|
||||
{contentTitle}
|
||||
<button
|
||||
className="close"
|
||||
onClick={this.closePopover}
|
||||
aria-label="close"
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.x} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="popover-dropdown-content" style={contentStyle}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { className, buttonContent, label } = this.props
|
||||
const cn = classNames('popover-dropdown-component', className)
|
||||
|
||||
return (
|
||||
<div className={cn}>
|
||||
<Button
|
||||
onClick={this.togglePopover}
|
||||
onButtonRef={this.onInvokeButtonRef}
|
||||
>
|
||||
<span className="popover-dropdown-button-label">{label}</span>
|
||||
<span className="button-content">{buttonContent}</span>
|
||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
||||
</Button>
|
||||
{this.renderPopover()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -299,7 +299,13 @@ export class Tooltip<T extends TooltipTarget> extends React.Component<
|
|||
}
|
||||
}
|
||||
|
||||
private updateMouseRect = (event: MouseEvent) => {
|
||||
this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20)
|
||||
}
|
||||
|
||||
private onTargetMouseEnter = (event: MouseEvent) => {
|
||||
this.updateMouseRect(event)
|
||||
|
||||
this.mouseOverTarget = true
|
||||
this.cancelHideTooltip()
|
||||
if (!this.state.show) {
|
||||
|
@ -308,7 +314,7 @@ export class Tooltip<T extends TooltipTarget> extends React.Component<
|
|||
}
|
||||
|
||||
private onTargetMouseMove = (event: MouseEvent) => {
|
||||
this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20)
|
||||
this.updateMouseRect(event)
|
||||
}
|
||||
|
||||
private onTargetMouseDown = (event: MouseEvent) => {
|
||||
|
|
|
@ -7,7 +7,9 @@ import { RebasePreview } from '../../models/rebase'
|
|||
import { Repository } from '../../models/repository'
|
||||
import { IDropdownSelectButtonOption } from '../dropdown-select-button'
|
||||
|
||||
export function getMergeOptions(): ReadonlyArray<IDropdownSelectButtonOption> {
|
||||
export function getMergeOptions(): ReadonlyArray<
|
||||
IDropdownSelectButtonOption<MultiCommitOperationKind>
|
||||
> {
|
||||
return [
|
||||
{
|
||||
label: 'Create a merge commit',
|
||||
|
|
|
@ -164,6 +164,9 @@ export const checkForUpdates = invokeProxy('check-for-updates', 1)
|
|||
/** Tell the main process to quit the app and install updates */
|
||||
export const quitAndInstallUpdate = sendProxy('quit-and-install-updates', 0)
|
||||
|
||||
/** Tell the main process to quit the app */
|
||||
export const quitApp = sendProxy('quit-app', 0)
|
||||
|
||||
/** Subscribes to auto updater error events originating from the main process */
|
||||
export function onAutoUpdaterError(
|
||||
errorHandler: (evt: Electron.IpcRendererEvent, error: Error) => void
|
||||
|
@ -200,6 +203,12 @@ export function onNativeThemeUpdated(eventHandler: () => void) {
|
|||
ipcRenderer.on('native-theme-updated', eventHandler)
|
||||
}
|
||||
|
||||
/** Subscribes to the "show installing update dialog" event originating from the
|
||||
* main process */
|
||||
export function onShowInstallingUpdate(eventHandler: () => void) {
|
||||
ipcRenderer.on('show-installing-update', eventHandler)
|
||||
}
|
||||
|
||||
/** Tell the main process to set the native theme source */
|
||||
export const setNativeThemeSource = sendProxy('set-native-theme-source', 1)
|
||||
|
||||
|
@ -273,6 +282,29 @@ export function sendWillQuitSync() {
|
|||
ipcRenderer.sendSync('will-quit')
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the main process that we're going to quit, even if the app is installing
|
||||
* an update. This means it should allow the window to close.
|
||||
*
|
||||
* This event is sent synchronously to avoid any races with subsequent calls
|
||||
* that would tell the app to quit.
|
||||
*/
|
||||
export function sendWillQuitEvenIfUpdatingSync() {
|
||||
// eslint-disable-next-line no-sync
|
||||
ipcRenderer.sendSync('will-quit-even-if-updating')
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the main process that the user cancelled quitting.
|
||||
*
|
||||
* This event is sent synchronously to avoid any races with subsequent calls
|
||||
* that would tell the app to quit.
|
||||
*/
|
||||
export function sendCancelQuittingSync() {
|
||||
// eslint-disable-next-line no-sync
|
||||
ipcRenderer.sendSync('cancel-quitting')
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the main process to move the application to the application folder
|
||||
*/
|
||||
|
|
|
@ -161,11 +161,12 @@ export abstract class BaseChooseBranchDialog extends React.Component<
|
|||
return currentBranch === defaultBranch ? null : defaultBranch
|
||||
}
|
||||
|
||||
private onOperationChange = (option: IDropdownSelectButtonOption) => {
|
||||
const value = option.value as MultiCommitOperationKind
|
||||
private onOperationChange = (
|
||||
option: IDropdownSelectButtonOption<MultiCommitOperationKind>
|
||||
) => {
|
||||
const { dispatcher, repository } = this.props
|
||||
const { selectedBranch } = this.state
|
||||
switch (value) {
|
||||
switch (option.value) {
|
||||
case MultiCommitOperationKind.Merge:
|
||||
dispatcher.startMergeBranchOperation(repository, false, selectedBranch)
|
||||
break
|
||||
|
@ -179,7 +180,7 @@ export abstract class BaseChooseBranchDialog extends React.Component<
|
|||
case MultiCommitOperationKind.Reorder:
|
||||
break
|
||||
default:
|
||||
assertNever(value, `Unknown operation value: ${option.value}`)
|
||||
assertNever(option.value, `Unknown operation value: ${option.value}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
import * as React from 'react'
|
||||
import { IPullRequestState } from '../../lib/app-state'
|
||||
import { IConstrainedValue, IPullRequestState } from '../../lib/app-state'
|
||||
import { getDotComAPIEndpoint } from '../../lib/api'
|
||||
import { Branch } from '../../models/branch'
|
||||
import { ImageDiffType } from '../../models/diff'
|
||||
import { Repository } from '../../models/repository'
|
||||
import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import { Ref } from '../lib/ref'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { OpenPullRequestDialogHeader } from './open-pull-request-header'
|
||||
import { PullRequestFilesChanged } from './pull-request-files-changed'
|
||||
import { PullRequestMergeStatus } from './pull-request-merge-status'
|
||||
import { ComputedAction } from '../../models/computed-action'
|
||||
|
||||
interface IOpenPullRequestDialogProps {
|
||||
readonly repository: Repository
|
||||
|
@ -26,14 +34,42 @@ interface IOpenPullRequestDialogProps {
|
|||
readonly defaultBranch: Branch | null
|
||||
|
||||
/**
|
||||
* See IBranchesState.allBranches
|
||||
* Branches in the repo with the repo's default remote
|
||||
*
|
||||
* We only want branches that are also on dotcom such that, when we ask a user
|
||||
* to create a pull request, the base branch also exists on dotcom.
|
||||
*/
|
||||
readonly allBranches: ReadonlyArray<Branch>
|
||||
readonly prBaseBranches: ReadonlyArray<Branch>
|
||||
|
||||
/**
|
||||
* See IBranchesState.recentBranches
|
||||
* Recent branches with the repo's default remote
|
||||
*
|
||||
* We only want branches that are also on dotcom such that, when we ask a user
|
||||
* to create a pull request, the base branch also exists on dotcom.
|
||||
*/
|
||||
readonly recentBranches: ReadonlyArray<Branch>
|
||||
readonly prRecentBaseBranches: ReadonlyArray<Branch>
|
||||
|
||||
/** Whether we should display side by side diffs. */
|
||||
readonly showSideBySideDiff: boolean
|
||||
|
||||
/** Whether we should hide whitespace in diff. */
|
||||
readonly hideWhitespaceInDiff: boolean
|
||||
|
||||
/** The type of image diff to display. */
|
||||
readonly imageDiffType: ImageDiffType
|
||||
|
||||
/** Label for selected external editor */
|
||||
readonly externalEditorLabel?: string
|
||||
|
||||
/** Width to use for the files list pane in the files changed view */
|
||||
readonly fileListWidth: IConstrainedValue
|
||||
|
||||
/** If the latest commit of the pull request is not local, this will contain
|
||||
* it's SHA */
|
||||
readonly nonLocalCommitSHA: string | null
|
||||
|
||||
/** Whether the current branch already has a pull request*/
|
||||
readonly currentBranchHasPullRequest: boolean
|
||||
|
||||
/** Called to dismiss the dialog */
|
||||
readonly onDismissed: () => void
|
||||
|
@ -42,9 +78,24 @@ interface IOpenPullRequestDialogProps {
|
|||
/** The component for start a pull request. */
|
||||
export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialogProps> {
|
||||
private onCreatePullRequest = () => {
|
||||
this.props.dispatcher.createPullRequest(this.props.repository)
|
||||
// TODO: create pr from dialog pr stat?
|
||||
this.props.dispatcher.recordCreatePullRequest()
|
||||
const { currentBranchHasPullRequest, dispatcher, repository, onDismissed } =
|
||||
this.props
|
||||
|
||||
if (currentBranchHasPullRequest) {
|
||||
dispatcher.showPullRequest(repository)
|
||||
} else {
|
||||
const { baseBranch } = this.props.pullRequestState
|
||||
dispatcher.createPullRequest(repository, baseBranch ?? undefined)
|
||||
dispatcher.recordCreatePullRequest()
|
||||
dispatcher.recordCreatePullRequestFromPreview()
|
||||
}
|
||||
|
||||
onDismissed()
|
||||
}
|
||||
|
||||
private onBranchChange = (branch: Branch) => {
|
||||
const { repository } = this.props
|
||||
this.props.dispatcher.updatePullRequestBaseBranch(repository, branch)
|
||||
}
|
||||
|
||||
private renderHeader() {
|
||||
|
@ -52,8 +103,8 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
|||
currentBranch,
|
||||
pullRequestState,
|
||||
defaultBranch,
|
||||
allBranches,
|
||||
recentBranches,
|
||||
prBaseBranches,
|
||||
prRecentBaseBranches,
|
||||
} = this.props
|
||||
const { baseBranch, commitSHAs } = pullRequestState
|
||||
return (
|
||||
|
@ -61,25 +112,152 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
|||
baseBranch={baseBranch}
|
||||
currentBranch={currentBranch}
|
||||
defaultBranch={defaultBranch}
|
||||
allBranches={allBranches}
|
||||
recentBranches={recentBranches}
|
||||
prBaseBranches={prBaseBranches}
|
||||
prRecentBaseBranches={prRecentBaseBranches}
|
||||
commitCount={commitSHAs?.length ?? 0}
|
||||
onBranchChange={this.onBranchChange}
|
||||
onDismissed={this.props.onDismissed}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderContent() {
|
||||
return <div>Content</div>
|
||||
return (
|
||||
<div className="open-pull-request-content">
|
||||
{this.renderNoChanges()}
|
||||
{this.renderNoDefaultBranch()}
|
||||
{this.renderFilesChanged()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderFilesChanged() {
|
||||
const {
|
||||
dispatcher,
|
||||
externalEditorLabel,
|
||||
hideWhitespaceInDiff,
|
||||
imageDiffType,
|
||||
pullRequestState,
|
||||
repository,
|
||||
fileListWidth,
|
||||
nonLocalCommitSHA,
|
||||
} = this.props
|
||||
const { commitSelection } = pullRequestState
|
||||
if (commitSelection === null) {
|
||||
// type checking - will render no default branch message
|
||||
return
|
||||
}
|
||||
|
||||
const { diff, file, changesetData, shas } = commitSelection
|
||||
const { files } = changesetData
|
||||
|
||||
if (shas.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<PullRequestFilesChanged
|
||||
diff={diff}
|
||||
dispatcher={dispatcher}
|
||||
externalEditorLabel={externalEditorLabel}
|
||||
fileListWidth={fileListWidth}
|
||||
files={files}
|
||||
hideWhitespaceInDiff={hideWhitespaceInDiff}
|
||||
imageDiffType={imageDiffType}
|
||||
nonLocalCommitSHA={nonLocalCommitSHA}
|
||||
selectedFile={file}
|
||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||
repository={repository}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderNoChanges() {
|
||||
const { pullRequestState, currentBranch } = this.props
|
||||
const { commitSelection, baseBranch, mergeStatus } = pullRequestState
|
||||
if (commitSelection === null || baseBranch === null) {
|
||||
// type checking - will render no default branch message
|
||||
return
|
||||
}
|
||||
|
||||
const { shas } = commitSelection
|
||||
if (shas.length !== 0) {
|
||||
return
|
||||
}
|
||||
const hasMergeBase = mergeStatus?.kind !== ComputedAction.Invalid
|
||||
const message = hasMergeBase ? (
|
||||
<>
|
||||
<Ref>{baseBranch.name}</Ref> is up to date with all commits from{' '}
|
||||
<Ref>{currentBranch.name}</Ref>.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Ref>{baseBranch.name}</Ref> and <Ref>{currentBranch.name}</Ref> are
|
||||
entirely different commit histories.
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<div className="open-pull-request-message">
|
||||
<div>
|
||||
<Octicon symbol={OcticonSymbol.gitPullRequest} />
|
||||
<h3>There are no changes.</h3>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderNoDefaultBranch() {
|
||||
const { baseBranch } = this.props.pullRequestState
|
||||
|
||||
if (baseBranch !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="open-pull-request-message">
|
||||
<div>
|
||||
<Octicon symbol={OcticonSymbol.gitPullRequest} />
|
||||
<h3>Could not find a default branch to compare against.</h3>
|
||||
Select a base branch above.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderFooter() {
|
||||
const { currentBranchHasPullRequest, pullRequestState, repository } =
|
||||
this.props
|
||||
const { mergeStatus, commitSHAs } = pullRequestState
|
||||
const gitHubRepository = repository.gitHubRepository
|
||||
const isEnterprise =
|
||||
gitHubRepository && gitHubRepository.endpoint !== getDotComAPIEndpoint()
|
||||
|
||||
const viewCreate = currentBranchHasPullRequest ? 'View' : ' Create'
|
||||
const buttonTitle = `${viewCreate} pull request on GitHub${
|
||||
isEnterprise ? ' Enterprise' : ''
|
||||
}.`
|
||||
|
||||
const okButton = (
|
||||
<>
|
||||
{currentBranchHasPullRequest && (
|
||||
<Octicon symbol={OcticonSymbol.linkExternal} />
|
||||
)}
|
||||
{__DARWIN__
|
||||
? `${viewCreate} Pull Request`
|
||||
: `${viewCreate} pull request`}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<DialogFooter>
|
||||
<PullRequestMergeStatus mergeStatus={mergeStatus} />
|
||||
|
||||
<OkCancelButtonGroup
|
||||
okButtonText="Create Pull Request"
|
||||
okButtonTitle="Create pull request on GitHub."
|
||||
okButtonText={okButton}
|
||||
okButtonTitle={buttonTitle}
|
||||
cancelButtonText="Cancel"
|
||||
okButtonDisabled={commitSHAs === null || commitSHAs.length === 0}
|
||||
/>
|
||||
</DialogFooter>
|
||||
)
|
||||
|
@ -93,8 +271,7 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
|||
onDismissed={this.props.onDismissed}
|
||||
>
|
||||
{this.renderHeader()}
|
||||
<div className="content">{this.renderContent()}</div>
|
||||
|
||||
{this.renderContent()}
|
||||
{this.renderFooter()}
|
||||
</Dialog>
|
||||
)
|
||||
|
|
|
@ -2,12 +2,12 @@ import * as React from 'react'
|
|||
import { Branch } from '../../models/branch'
|
||||
import { BranchSelect } from '../branches/branch-select'
|
||||
import { DialogHeader } from '../dialog/header'
|
||||
import { createUniqueId } from '../lib/id-pool'
|
||||
import { createUniqueId, releaseUniqueId } from '../lib/id-pool'
|
||||
import { Ref } from '../lib/ref'
|
||||
|
||||
interface IOpenPullRequestDialogHeaderProps {
|
||||
/** The base branch of the pull request */
|
||||
readonly baseBranch: Branch
|
||||
readonly baseBranch: Branch | null
|
||||
|
||||
/** The branch of the pull request */
|
||||
readonly currentBranch: Branch
|
||||
|
@ -18,18 +18,27 @@ interface IOpenPullRequestDialogHeaderProps {
|
|||
readonly defaultBranch: Branch | null
|
||||
|
||||
/**
|
||||
* See IBranchesState.allBranches
|
||||
* Branches in the repo with the repo's default remote
|
||||
*
|
||||
* We only want branches that are also on dotcom such that, when we ask a user
|
||||
* to create a pull request, the base branch also exists on dotcom.
|
||||
*/
|
||||
readonly allBranches: ReadonlyArray<Branch>
|
||||
readonly prBaseBranches: ReadonlyArray<Branch>
|
||||
|
||||
/**
|
||||
* See IBranchesState.recentBranches
|
||||
* Recent branches with the repo's default remote
|
||||
*
|
||||
* We only want branches that are also on dotcom such that, when we ask a user
|
||||
* to create a pull request, the base branch also exists on dotcom.
|
||||
*/
|
||||
readonly recentBranches: ReadonlyArray<Branch>
|
||||
readonly prRecentBaseBranches: ReadonlyArray<Branch>
|
||||
|
||||
/** The count of commits of the pull request */
|
||||
readonly commitCount: number
|
||||
|
||||
/** When the branch selection changes */
|
||||
readonly onBranchChange: (branch: Branch) => void
|
||||
|
||||
/**
|
||||
* Event triggered when the dialog is dismissed by the user in the
|
||||
* ways described in the dismissable prop.
|
||||
|
@ -37,23 +46,43 @@ interface IOpenPullRequestDialogHeaderProps {
|
|||
readonly onDismissed?: () => void
|
||||
}
|
||||
|
||||
interface IOpenPullRequestDialogHeaderState {
|
||||
/**
|
||||
* An id for the h1 element that contains the title of this dialog. Used to
|
||||
* aid in accessibility by allowing the h1 to be referenced in an
|
||||
* aria-labeledby/aria-describedby attributed. Undefined if the dialog does
|
||||
* not have a title or the component has not yet been mounted.
|
||||
*/
|
||||
readonly titleId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A header component for the open pull request dialog. Made to house the
|
||||
* base branch dropdown and merge details common to all pull request views.
|
||||
*/
|
||||
export class OpenPullRequestDialogHeader extends React.Component<
|
||||
IOpenPullRequestDialogHeaderProps,
|
||||
{}
|
||||
IOpenPullRequestDialogHeaderState
|
||||
> {
|
||||
public constructor(props: IOpenPullRequestDialogHeaderProps) {
|
||||
super(props)
|
||||
this.state = { titleId: createUniqueId(`Dialog_Open_Pull_Request`) }
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
releaseUniqueId(this.state.titleId)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const title = __DARWIN__ ? 'Open a Pull Request' : 'Open a pull request'
|
||||
const {
|
||||
baseBranch,
|
||||
currentBranch,
|
||||
defaultBranch,
|
||||
allBranches,
|
||||
recentBranches,
|
||||
prBaseBranches,
|
||||
prRecentBaseBranches,
|
||||
commitCount,
|
||||
onBranchChange,
|
||||
onDismissed,
|
||||
} = this.props
|
||||
const commits = `${commitCount} commit${commitCount > 1 ? 's' : ''}`
|
||||
|
@ -61,7 +90,7 @@ export class OpenPullRequestDialogHeader extends React.Component<
|
|||
return (
|
||||
<DialogHeader
|
||||
title={title}
|
||||
titleId={createUniqueId(`Dialog_${title}_${title}`)}
|
||||
titleId={this.state.titleId}
|
||||
dismissable={true}
|
||||
onDismissed={onDismissed}
|
||||
>
|
||||
|
@ -72,8 +101,15 @@ export class OpenPullRequestDialogHeader extends React.Component<
|
|||
branch={baseBranch}
|
||||
defaultBranch={defaultBranch}
|
||||
currentBranch={currentBranch}
|
||||
allBranches={allBranches}
|
||||
recentBranches={recentBranches}
|
||||
allBranches={prBaseBranches}
|
||||
recentBranches={prRecentBaseBranches}
|
||||
onChange={onBranchChange}
|
||||
noBranchesMessage={
|
||||
<>
|
||||
Sorry, I can't find that remote branch. <br />
|
||||
You can only open pull requests against remote branches.
|
||||
</>
|
||||
}
|
||||
/>{' '}
|
||||
from <Ref>{currentBranch.name}</Ref>.
|
||||
</div>
|
||||
|
|
307
app/src/ui/open-pull-request/pull-request-files-changed.tsx
Normal file
307
app/src/ui/open-pull-request/pull-request-files-changed.tsx
Normal file
|
@ -0,0 +1,307 @@
|
|||
import * as React from 'react'
|
||||
import * as Path from 'path'
|
||||
import { IDiff, ImageDiffType } from '../../models/diff'
|
||||
import { Repository } from '../../models/repository'
|
||||
import { CommittedFileChange } from '../../models/status'
|
||||
import { SeamlessDiffSwitcher } from '../diff/seamless-diff-switcher'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import { openFile } from '../lib/open-file'
|
||||
import { Resizable } from '../resizable'
|
||||
import { FileList } from '../history/file-list'
|
||||
import { IMenuItem, showContextualMenu } from '../../lib/menu-item'
|
||||
import { pathExists } from '../lib/path-exists'
|
||||
import {
|
||||
CopyFilePathLabel,
|
||||
CopyRelativeFilePathLabel,
|
||||
DefaultEditorLabel,
|
||||
isSafeFileExtension,
|
||||
OpenWithDefaultProgramLabel,
|
||||
RevealInFileManagerLabel,
|
||||
} from '../lib/context-menu'
|
||||
import { revealInFileManager } from '../../lib/app-shell'
|
||||
import { clipboard } from 'electron'
|
||||
import { IConstrainedValue } from '../../lib/app-state'
|
||||
import { clamp } from '../../lib/clamp'
|
||||
import { getDotComAPIEndpoint } from '../../lib/api'
|
||||
import { createCommitURL } from '../../lib/commit-url'
|
||||
import { DiffOptions } from '../diff/diff-options'
|
||||
|
||||
interface IPullRequestFilesChangedProps {
|
||||
readonly repository: Repository
|
||||
readonly dispatcher: Dispatcher
|
||||
|
||||
/** The file whose diff should be displayed. */
|
||||
readonly selectedFile: CommittedFileChange | null
|
||||
|
||||
/** The files changed in the pull request. */
|
||||
readonly files: ReadonlyArray<CommittedFileChange>
|
||||
|
||||
/** The diff that should be rendered */
|
||||
readonly diff: IDiff | null
|
||||
|
||||
/** The type of image diff to display. */
|
||||
readonly imageDiffType: ImageDiffType
|
||||
|
||||
/** Whether we should display side by side diffs. */
|
||||
readonly showSideBySideDiff: boolean
|
||||
|
||||
/** Whether we should hide whitespace in diff. */
|
||||
readonly hideWhitespaceInDiff: boolean
|
||||
|
||||
/** Label for selected external editor */
|
||||
readonly externalEditorLabel?: string
|
||||
|
||||
/** Width to use for the files list pane */
|
||||
readonly fileListWidth: IConstrainedValue
|
||||
|
||||
/** If the latest commit of the pull request is not local, this will contain
|
||||
* it's SHA */
|
||||
readonly nonLocalCommitSHA: string | null
|
||||
}
|
||||
|
||||
interface IPullRequestFilesChangedState {
|
||||
readonly showSideBySideDiff: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for viewing the file changes for a pull request.
|
||||
*/
|
||||
export class PullRequestFilesChanged extends React.Component<
|
||||
IPullRequestFilesChangedProps,
|
||||
IPullRequestFilesChangedState
|
||||
> {
|
||||
public constructor(props: IPullRequestFilesChangedProps) {
|
||||
super(props)
|
||||
|
||||
this.state = { showSideBySideDiff: props.showSideBySideDiff }
|
||||
}
|
||||
|
||||
private onOpenFile = (path: string) => {
|
||||
const fullPath = Path.join(this.props.repository.path, path)
|
||||
this.onOpenBinaryFile(fullPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a binary file in an the system-assigned application for
|
||||
* said file type.
|
||||
*/
|
||||
private onOpenBinaryFile = (fullPath: string) => {
|
||||
openFile(fullPath, this.props.dispatcher)
|
||||
}
|
||||
|
||||
/** Called when the user changes the hide whitespace in diffs setting. */
|
||||
private onHideWhitespaceInDiffChanged = (hideWhitespaceInDiff: boolean) => {
|
||||
const { selectedFile } = this.props
|
||||
return this.props.dispatcher.onHideWhitespaceInPullRequestDiffChanged(
|
||||
hideWhitespaceInDiff,
|
||||
this.props.repository,
|
||||
selectedFile
|
||||
)
|
||||
}
|
||||
|
||||
private onShowSideBySideDiffChanged = (showSideBySideDiff: boolean) => {
|
||||
this.setState({ showSideBySideDiff })
|
||||
}
|
||||
|
||||
private onDiffOptionsOpened = () => {
|
||||
this.props.dispatcher.recordDiffOptionsViewed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user is viewing an image diff and requests
|
||||
* to change the diff presentation mode.
|
||||
*/
|
||||
private onChangeImageDiffType = (imageDiffType: ImageDiffType) => {
|
||||
this.props.dispatcher.changeImageDiffType(imageDiffType)
|
||||
}
|
||||
|
||||
private onFileListResize = (width: number) => {
|
||||
this.props.dispatcher.setPullRequestFileListWidth(width)
|
||||
}
|
||||
|
||||
private onFileListSizeReset = () => {
|
||||
this.props.dispatcher.resetPullRequestFileListWidth()
|
||||
}
|
||||
|
||||
private onViewOnGitHub = (file: CommittedFileChange) => {
|
||||
const { nonLocalCommitSHA, repository, dispatcher } = this.props
|
||||
const { gitHubRepository } = repository
|
||||
|
||||
if (gitHubRepository === null || nonLocalCommitSHA === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const commitURL = createCommitURL(
|
||||
gitHubRepository,
|
||||
nonLocalCommitSHA,
|
||||
file.path
|
||||
)
|
||||
|
||||
if (commitURL === null) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatcher.openInBrowser(commitURL)
|
||||
}
|
||||
|
||||
private onFileContextMenu = async (
|
||||
file: CommittedFileChange,
|
||||
event: React.MouseEvent<HTMLDivElement>
|
||||
) => {
|
||||
event.preventDefault()
|
||||
|
||||
const { repository } = this.props
|
||||
|
||||
const fullPath = Path.join(repository.path, file.path)
|
||||
const fileExistsOnDisk = await pathExists(fullPath)
|
||||
if (!fileExistsOnDisk) {
|
||||
showContextualMenu([
|
||||
{
|
||||
label: __DARWIN__
|
||||
? 'File Does Not Exist on Disk'
|
||||
: 'File does not exist on disk',
|
||||
enabled: false,
|
||||
},
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
const { externalEditorLabel, dispatcher } = this.props
|
||||
|
||||
const extension = Path.extname(file.path)
|
||||
const isSafeExtension = isSafeFileExtension(extension)
|
||||
const openInExternalEditor =
|
||||
externalEditorLabel !== undefined
|
||||
? `Open in ${externalEditorLabel}`
|
||||
: DefaultEditorLabel
|
||||
|
||||
const items: IMenuItem[] = [
|
||||
{
|
||||
label: RevealInFileManagerLabel,
|
||||
action: () => revealInFileManager(repository, file.path),
|
||||
enabled: fileExistsOnDisk,
|
||||
},
|
||||
{
|
||||
label: openInExternalEditor,
|
||||
action: () => dispatcher.openInExternalEditor(fullPath),
|
||||
enabled: fileExistsOnDisk,
|
||||
},
|
||||
{
|
||||
label: OpenWithDefaultProgramLabel,
|
||||
action: () => this.onOpenFile(file.path),
|
||||
enabled: isSafeExtension && fileExistsOnDisk,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: CopyFilePathLabel,
|
||||
action: () => clipboard.writeText(fullPath),
|
||||
},
|
||||
{
|
||||
label: CopyRelativeFilePathLabel,
|
||||
action: () => clipboard.writeText(Path.normalize(file.path)),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
]
|
||||
|
||||
const { nonLocalCommitSHA } = this.props
|
||||
const { gitHubRepository } = repository
|
||||
const isEnterprise =
|
||||
gitHubRepository && gitHubRepository.endpoint !== getDotComAPIEndpoint()
|
||||
|
||||
items.push({
|
||||
label: `View on GitHub${isEnterprise ? ' Enterprise' : ''}`,
|
||||
action: () => this.onViewOnGitHub(file),
|
||||
enabled: nonLocalCommitSHA !== null && gitHubRepository !== null,
|
||||
})
|
||||
|
||||
showContextualMenu(items)
|
||||
}
|
||||
|
||||
private onFileSelected = (file: CommittedFileChange) => {
|
||||
this.props.dispatcher.changePullRequestFileSelection(
|
||||
this.props.repository,
|
||||
file
|
||||
)
|
||||
}
|
||||
|
||||
private renderHeader() {
|
||||
const { hideWhitespaceInDiff } = this.props
|
||||
const { showSideBySideDiff } = this.state
|
||||
return (
|
||||
<div className="files-changed-header">
|
||||
<div className="commits-displayed">
|
||||
Showing changes from all commits
|
||||
</div>
|
||||
<DiffOptions
|
||||
isInteractiveDiff={false}
|
||||
hideWhitespaceChanges={hideWhitespaceInDiff}
|
||||
onHideWhitespaceChangesChanged={this.onHideWhitespaceInDiffChanged}
|
||||
showSideBySideDiff={showSideBySideDiff}
|
||||
onShowSideBySideDiffChanged={this.onShowSideBySideDiffChanged}
|
||||
onDiffOptionsOpened={this.onDiffOptionsOpened}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderFileList() {
|
||||
const { files, selectedFile, fileListWidth } = this.props
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
width={fileListWidth.value}
|
||||
minimumWidth={fileListWidth.min}
|
||||
maximumWidth={fileListWidth.max}
|
||||
onResize={this.onFileListResize}
|
||||
onReset={this.onFileListSizeReset}
|
||||
>
|
||||
<FileList
|
||||
files={files}
|
||||
onSelectedFileChanged={this.onFileSelected}
|
||||
selectedFile={selectedFile}
|
||||
availableWidth={clamp(fileListWidth)}
|
||||
onContextMenu={this.onFileContextMenu}
|
||||
/>
|
||||
</Resizable>
|
||||
)
|
||||
}
|
||||
|
||||
private renderDiff() {
|
||||
const { selectedFile } = this.props
|
||||
|
||||
if (selectedFile === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { diff, repository, imageDiffType, hideWhitespaceInDiff } = this.props
|
||||
|
||||
const { showSideBySideDiff } = this.state
|
||||
|
||||
return (
|
||||
<SeamlessDiffSwitcher
|
||||
repository={repository}
|
||||
imageDiffType={imageDiffType}
|
||||
file={selectedFile}
|
||||
diff={diff}
|
||||
readOnly={true}
|
||||
hideWhitespaceInDiff={hideWhitespaceInDiff}
|
||||
showSideBySideDiff={showSideBySideDiff}
|
||||
onOpenBinaryFile={this.onOpenBinaryFile}
|
||||
onChangeImageDiffType={this.onChangeImageDiffType}
|
||||
onHideWhitespaceInDiffChanged={this.onHideWhitespaceInDiffChanged}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="pull-request-files-changed">
|
||||
{this.renderHeader()}
|
||||
<div className="files-diff-viewer">
|
||||
{this.renderFileList()}
|
||||
{this.renderDiff()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
68
app/src/ui/open-pull-request/pull-request-merge-status.tsx
Normal file
68
app/src/ui/open-pull-request/pull-request-merge-status.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import * as React from 'react'
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
import { ComputedAction } from '../../models/computed-action'
|
||||
import { MergeTreeResult } from '../../models/merge'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
|
||||
interface IPullRequestMergeStatusProps {
|
||||
/** The result of merging the pull request branch into the base branch */
|
||||
readonly mergeStatus: MergeTreeResult | null
|
||||
}
|
||||
|
||||
/** The component to display message about the result of merging the pull
|
||||
* request. */
|
||||
export class PullRequestMergeStatus extends React.Component<IPullRequestMergeStatusProps> {
|
||||
private getMergeStatusDescription = () => {
|
||||
const { mergeStatus } = this.props
|
||||
if (mergeStatus === null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const { kind } = mergeStatus
|
||||
switch (kind) {
|
||||
case ComputedAction.Loading:
|
||||
return (
|
||||
<span className="pr-merge-status-loading">
|
||||
<strong>Checking mergeability…</strong> Don’t worry, you can
|
||||
still create the pull request.
|
||||
</span>
|
||||
)
|
||||
case ComputedAction.Invalid:
|
||||
return (
|
||||
<span className="pr-merge-status-invalid">
|
||||
<strong>Error checking merge status.</strong> Unable to merge
|
||||
unrelated histories in this repository
|
||||
</span>
|
||||
)
|
||||
case ComputedAction.Clean:
|
||||
return (
|
||||
<span className="pr-merge-status-clean">
|
||||
<strong>
|
||||
<Octicon symbol={OcticonSymbol.check} /> Able to merge.
|
||||
</strong>{' '}
|
||||
These branches can be automatically merged.
|
||||
</span>
|
||||
)
|
||||
case ComputedAction.Conflicts:
|
||||
return (
|
||||
<span className="pr-merge-status-conflicts">
|
||||
<strong>
|
||||
<Octicon symbol={OcticonSymbol.x} /> Can't automatically merge.
|
||||
</strong>{' '}
|
||||
Don’t worry, you can still create the pull request.
|
||||
</span>
|
||||
)
|
||||
default:
|
||||
return assertNever(kind, `Unknown merge status kind of ${kind}.`)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="pull-request-merge-status">
|
||||
{this.getMergeStatusDescription()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ interface IPreferencesProps {
|
|||
readonly confirmRepositoryRemoval: boolean
|
||||
readonly confirmDiscardChanges: boolean
|
||||
readonly confirmDiscardChangesPermanently: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
readonly confirmForcePush: boolean
|
||||
readonly confirmUndoCommit: boolean
|
||||
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
||||
|
@ -79,6 +80,7 @@ interface IPreferencesState {
|
|||
readonly confirmRepositoryRemoval: boolean
|
||||
readonly confirmDiscardChanges: boolean
|
||||
readonly confirmDiscardChangesPermanently: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
readonly confirmForcePush: boolean
|
||||
readonly confirmUndoCommit: boolean
|
||||
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
||||
|
@ -121,6 +123,7 @@ export class Preferences extends React.Component<
|
|||
confirmRepositoryRemoval: false,
|
||||
confirmDiscardChanges: false,
|
||||
confirmDiscardChangesPermanently: false,
|
||||
confirmDiscardStash: false,
|
||||
confirmForcePush: false,
|
||||
confirmUndoCommit: false,
|
||||
uncommittedChangesStrategy: defaultUncommittedChangesStrategy,
|
||||
|
@ -178,6 +181,7 @@ export class Preferences extends React.Component<
|
|||
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
||||
confirmDiscardChangesPermanently:
|
||||
this.props.confirmDiscardChangesPermanently,
|
||||
confirmDiscardStash: this.props.confirmDiscardStash,
|
||||
confirmForcePush: this.props.confirmForcePush,
|
||||
confirmUndoCommit: this.props.confirmUndoCommit,
|
||||
uncommittedChangesStrategy: this.props.uncommittedChangesStrategy,
|
||||
|
@ -333,12 +337,14 @@ export class Preferences extends React.Component<
|
|||
confirmDiscardChangesPermanently={
|
||||
this.state.confirmDiscardChangesPermanently
|
||||
}
|
||||
confirmDiscardStash={this.state.confirmDiscardStash}
|
||||
confirmForcePush={this.state.confirmForcePush}
|
||||
confirmUndoCommit={this.state.confirmUndoCommit}
|
||||
onConfirmRepositoryRemovalChanged={
|
||||
this.onConfirmRepositoryRemovalChanged
|
||||
}
|
||||
onConfirmDiscardChangesChanged={this.onConfirmDiscardChangesChanged}
|
||||
onConfirmDiscardStashChanged={this.onConfirmDiscardStashChanged}
|
||||
onConfirmForcePushChanged={this.onConfirmForcePushChanged}
|
||||
onConfirmDiscardChangesPermanentlyChanged={
|
||||
this.onConfirmDiscardChangesPermanentlyChanged
|
||||
|
@ -410,6 +416,10 @@ export class Preferences extends React.Component<
|
|||
this.setState({ confirmDiscardChanges: value })
|
||||
}
|
||||
|
||||
private onConfirmDiscardStashChanged = (value: boolean) => {
|
||||
this.setState({ confirmDiscardStash: value })
|
||||
}
|
||||
|
||||
private onConfirmDiscardChangesPermanentlyChanged = (value: boolean) => {
|
||||
this.setState({ confirmDiscardChangesPermanently: value })
|
||||
}
|
||||
|
@ -562,6 +572,10 @@ export class Preferences extends React.Component<
|
|||
this.state.confirmForcePush
|
||||
)
|
||||
|
||||
await this.props.dispatcher.setConfirmDiscardStashSetting(
|
||||
this.state.confirmDiscardStash
|
||||
)
|
||||
|
||||
await this.props.dispatcher.setConfirmUndoCommitSetting(
|
||||
this.state.confirmUndoCommit
|
||||
)
|
||||
|
|
|
@ -6,10 +6,12 @@ interface IPromptsPreferencesProps {
|
|||
readonly confirmRepositoryRemoval: boolean
|
||||
readonly confirmDiscardChanges: boolean
|
||||
readonly confirmDiscardChangesPermanently: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
readonly confirmForcePush: boolean
|
||||
readonly confirmUndoCommit: boolean
|
||||
readonly onConfirmDiscardChangesChanged: (checked: boolean) => void
|
||||
readonly onConfirmDiscardChangesPermanentlyChanged: (checked: boolean) => void
|
||||
readonly onConfirmDiscardStashChanged: (checked: boolean) => void
|
||||
readonly onConfirmRepositoryRemovalChanged: (checked: boolean) => void
|
||||
readonly onConfirmForcePushChanged: (checked: boolean) => void
|
||||
readonly onConfirmUndoCommitChanged: (checked: boolean) => void
|
||||
|
@ -19,6 +21,7 @@ interface IPromptsPreferencesState {
|
|||
readonly confirmRepositoryRemoval: boolean
|
||||
readonly confirmDiscardChanges: boolean
|
||||
readonly confirmDiscardChangesPermanently: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
readonly confirmForcePush: boolean
|
||||
readonly confirmUndoCommit: boolean
|
||||
}
|
||||
|
@ -35,6 +38,7 @@ export class Prompts extends React.Component<
|
|||
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
||||
confirmDiscardChangesPermanently:
|
||||
this.props.confirmDiscardChangesPermanently,
|
||||
confirmDiscardStash: this.props.confirmDiscardStash,
|
||||
confirmForcePush: this.props.confirmForcePush,
|
||||
confirmUndoCommit: this.props.confirmUndoCommit,
|
||||
}
|
||||
|
@ -58,6 +62,15 @@ export class Prompts extends React.Component<
|
|||
this.props.onConfirmDiscardChangesPermanentlyChanged(value)
|
||||
}
|
||||
|
||||
private onConfirmDiscardStashChanged = (
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
const value = event.currentTarget.checked
|
||||
|
||||
this.setState({ confirmDiscardStash: value })
|
||||
this.props.onConfirmDiscardStashChanged(value)
|
||||
}
|
||||
|
||||
private onConfirmForcePushChanged = (
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
|
@ -116,6 +129,15 @@ export class Prompts extends React.Component<
|
|||
}
|
||||
onChange={this.onConfirmDiscardChangesPermanentlyChanged}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Discarding stash"
|
||||
value={
|
||||
this.state.confirmDiscardStash
|
||||
? CheckboxValue.On
|
||||
: CheckboxValue.Off
|
||||
}
|
||||
onChange={this.onConfirmDiscardStashChanged}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Force pushing"
|
||||
value={
|
||||
|
|
|
@ -193,10 +193,14 @@ const renderAheadBehindIndicator = (aheadBehind: IAheadBehind) => {
|
|||
'its tracked branch.'
|
||||
|
||||
return (
|
||||
<div className="ahead-behind" title={aheadBehindTooltip}>
|
||||
<TooltippedContent
|
||||
className="ahead-behind"
|
||||
tagName="div"
|
||||
tooltip={aheadBehindTooltip}
|
||||
>
|
||||
{ahead > 0 && <Octicon symbol={OcticonSymbol.arrowUp} />}
|
||||
{behind > 0 && <Octicon symbol={OcticonSymbol.arrowDown} />}
|
||||
</div>
|
||||
</TooltippedContent>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import { AheadBehindStore } from '../lib/stores/ahead-behind-store'
|
|||
import { dragAndDropManager } from '../lib/drag-and-drop-manager'
|
||||
import { DragType } from '../models/drag-drop'
|
||||
import { clamp } from '../lib/clamp'
|
||||
import { PullRequestSuggestedNextAction } from '../models/pull-request'
|
||||
|
||||
interface IRepositoryViewProps {
|
||||
readonly repository: Repository
|
||||
|
@ -49,6 +50,7 @@ interface IRepositoryViewProps {
|
|||
readonly hideWhitespaceInHistoryDiff: boolean
|
||||
readonly showSideBySideDiff: boolean
|
||||
readonly askForConfirmationOnDiscardChanges: boolean
|
||||
readonly askForConfirmationOnDiscardStash: boolean
|
||||
readonly focusCommitMessage: boolean
|
||||
readonly commitSpellcheckEnabled: boolean
|
||||
readonly accounts: ReadonlyArray<Account>
|
||||
|
@ -91,6 +93,9 @@ interface IRepositoryViewProps {
|
|||
repository: Repository,
|
||||
commits: ReadonlyArray<CommitOneLine>
|
||||
) => void
|
||||
|
||||
/** The user's preference of pull request suggested next action to use **/
|
||||
readonly pullRequestSuggestedNextAction?: PullRequestSuggestedNextAction
|
||||
}
|
||||
|
||||
interface IRepositoryViewState {
|
||||
|
@ -350,6 +355,9 @@ export class RepositoryView extends React.Component<
|
|||
fileListWidth={this.props.stashedFilesWidth}
|
||||
repository={this.props.repository}
|
||||
dispatcher={this.props.dispatcher}
|
||||
askForConfirmationOnDiscardStash={
|
||||
this.props.askForConfirmationOnDiscardStash
|
||||
}
|
||||
isWorkingTreeClean={isWorkingTreeClean}
|
||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||
onOpenBinaryFile={this.onOpenBinaryFile}
|
||||
|
@ -461,6 +469,9 @@ export class RepositoryView extends React.Component<
|
|||
this.props.externalEditorLabel !== undefined
|
||||
}
|
||||
dispatcher={this.props.dispatcher}
|
||||
pullRequestSuggestedNextAction={
|
||||
this.props.pullRequestSuggestedNextAction
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,16 +5,19 @@ import { Dispatcher } from '../dispatcher'
|
|||
import { Row } from '../lib/row'
|
||||
import { IStashEntry } from '../../models/stash-entry'
|
||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
||||
|
||||
interface IConfirmDiscardStashProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
readonly repository: Repository
|
||||
readonly stash: IStashEntry
|
||||
readonly askForConfirmationOnDiscardStash: boolean
|
||||
readonly onDismissed: () => void
|
||||
}
|
||||
|
||||
interface IConfirmDiscardStashState {
|
||||
readonly isDiscarding: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
}
|
||||
/**
|
||||
* Dialog to confirm dropping a stash
|
||||
|
@ -28,6 +31,7 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
|||
|
||||
this.state = {
|
||||
isDiscarding: false,
|
||||
confirmDiscardStash: props.askForConfirmationOnDiscardStash,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +50,17 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
|||
>
|
||||
<DialogContent>
|
||||
<Row>Are you sure you want to discard these stashed changes?</Row>
|
||||
<Row>
|
||||
<Checkbox
|
||||
label="Do not show this message again"
|
||||
value={
|
||||
this.state.confirmDiscardStash
|
||||
? CheckboxValue.Off
|
||||
: CheckboxValue.On
|
||||
}
|
||||
onChange={this.onAskForConfirmationOnDiscardStashChanged}
|
||||
/>
|
||||
</Row>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<OkCancelButtonGroup destructive={true} okButtonText="Discard" />
|
||||
|
@ -54,6 +69,14 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private onAskForConfirmationOnDiscardStashChanged = (
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
const value = !event.currentTarget.checked
|
||||
|
||||
this.setState({ confirmDiscardStash: value })
|
||||
}
|
||||
|
||||
private onSubmit = async () => {
|
||||
const { dispatcher, repository, stash, onDismissed } = this.props
|
||||
|
||||
|
@ -62,6 +85,7 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
|||
})
|
||||
|
||||
try {
|
||||
dispatcher.setConfirmDiscardStashSetting(this.state.confirmDiscardStash)
|
||||
await dispatcher.dropStash(repository, stash)
|
||||
} finally {
|
||||
this.setState({
|
||||
|
|
|
@ -11,11 +11,13 @@ interface IStashDiffHeaderProps {
|
|||
readonly stashEntry: IStashEntry
|
||||
readonly repository: Repository
|
||||
readonly dispatcher: Dispatcher
|
||||
readonly askForConfirmationOnDiscardStash: boolean
|
||||
readonly isWorkingTreeClean: boolean
|
||||
}
|
||||
|
||||
interface IStashDiffHeaderState {
|
||||
readonly isRestoring: boolean
|
||||
readonly isDiscarding: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,12 +33,13 @@ export class StashDiffHeader extends React.Component<
|
|||
|
||||
this.state = {
|
||||
isRestoring: false,
|
||||
isDiscarding: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isWorkingTreeClean } = this.props
|
||||
const { isRestoring } = this.state
|
||||
const { isRestoring, isDiscarding } = this.state
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
|
@ -44,10 +47,12 @@ export class StashDiffHeader extends React.Component<
|
|||
<div className="row">
|
||||
<OkCancelButtonGroup
|
||||
okButtonText="Restore"
|
||||
okButtonDisabled={isRestoring || !isWorkingTreeClean}
|
||||
okButtonDisabled={
|
||||
isRestoring || !isWorkingTreeClean || isDiscarding
|
||||
}
|
||||
onOkButtonClick={this.onRestoreClick}
|
||||
cancelButtonText="Discard"
|
||||
cancelButtonDisabled={isRestoring}
|
||||
cancelButtonDisabled={isRestoring || isDiscarding}
|
||||
onCancelButtonClick={this.onDiscardClick}
|
||||
/>
|
||||
{this.renderExplanatoryText()}
|
||||
|
@ -80,13 +85,33 @@ export class StashDiffHeader extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private onDiscardClick = () => {
|
||||
const { dispatcher, repository, stashEntry } = this.props
|
||||
dispatcher.showPopup({
|
||||
type: PopupType.ConfirmDiscardStash,
|
||||
stash: stashEntry,
|
||||
private onDiscardClick = async () => {
|
||||
const {
|
||||
dispatcher,
|
||||
repository,
|
||||
})
|
||||
stashEntry,
|
||||
askForConfirmationOnDiscardStash,
|
||||
} = this.props
|
||||
|
||||
if (!askForConfirmationOnDiscardStash) {
|
||||
this.setState({
|
||||
isDiscarding: true,
|
||||
})
|
||||
|
||||
try {
|
||||
await dispatcher.dropStash(repository, stashEntry)
|
||||
} finally {
|
||||
this.setState({
|
||||
isDiscarding: false,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
dispatcher.showPopup({
|
||||
type: PopupType.ConfirmDiscardStash,
|
||||
stash: stashEntry,
|
||||
repository,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private onRestoreClick = async () => {
|
||||
|
|
|
@ -27,6 +27,9 @@ interface IStashDiffViewerProps {
|
|||
readonly repository: Repository
|
||||
readonly dispatcher: Dispatcher
|
||||
|
||||
/** Should the app propt the user to confirm a discard stash */
|
||||
readonly askForConfirmationOnDiscardStash: boolean
|
||||
|
||||
/** Whether we should display side by side diffs. */
|
||||
readonly showSideBySideDiff: boolean
|
||||
|
||||
|
@ -113,6 +116,9 @@ export class StashDiffViewer extends React.PureComponent<IStashDiffViewerProps>
|
|||
repository={repository}
|
||||
dispatcher={dispatcher}
|
||||
isWorkingTreeClean={isWorkingTreeClean}
|
||||
askForConfirmationOnDiscardStash={
|
||||
this.props.askForConfirmationOnDiscardStash
|
||||
}
|
||||
/>
|
||||
<div className="commit-details">
|
||||
<Resizable
|
||||
|
|
170
app/src/ui/suggested-actions/dropdown-suggested-action.tsx
Normal file
170
app/src/ui/suggested-actions/dropdown-suggested-action.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
DropdownSelectButton,
|
||||
IDropdownSelectButtonOption,
|
||||
} from '../dropdown-select-button'
|
||||
import { MenuIDs } from '../../models/menu-ids'
|
||||
import { executeMenuItemById } from '../main-process-proxy'
|
||||
import { sendNonFatalException } from '../../lib/helpers/non-fatal-exception'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export interface IDropdownSuggestedActionOption<T extends string>
|
||||
extends IDropdownSelectButtonOption<T> {
|
||||
/**
|
||||
* The title, or "header" text for a suggested
|
||||
* action.
|
||||
*/
|
||||
readonly title?: string
|
||||
|
||||
/**
|
||||
* A text or set of elements used to present information
|
||||
* to the user about how and where to access the action
|
||||
* outside of the suggested action.
|
||||
*/
|
||||
readonly discoverabilityContent?: string | JSX.Element
|
||||
|
||||
/**
|
||||
* A callback which is invoked when the user clicks
|
||||
* or activates the action using their keyboard.
|
||||
*/
|
||||
readonly onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
|
||||
/**
|
||||
* Whether or not the action should be disabled. Disabling
|
||||
* the action means that the button will no longer be
|
||||
* clickable.
|
||||
*/
|
||||
readonly disabled?: boolean
|
||||
|
||||
/**
|
||||
* An image to illustrate what this component's action does
|
||||
*/
|
||||
readonly image?: JSX.Element
|
||||
|
||||
/**
|
||||
* The id of the menu item backing this action.
|
||||
* When the action is invoked the menu item specified
|
||||
* by this id will be executed.
|
||||
*/
|
||||
readonly menuItemId?: MenuIDs
|
||||
}
|
||||
|
||||
export interface IDropdownSuggestedActionProps<T extends string> {
|
||||
/** The possible suggested next actions to select from
|
||||
*
|
||||
* This component assumes this is not an empty array.
|
||||
*/
|
||||
readonly suggestedActions: ReadonlyArray<IDropdownSuggestedActionOption<T>>
|
||||
|
||||
/** The value of the selected next action to initialize the component with */
|
||||
readonly selectedActionValue?: T
|
||||
|
||||
readonly onSuggestedActionChanged: (action: T) => void
|
||||
|
||||
/**
|
||||
* An optional additional class name to set in order to be able to apply
|
||||
* specific styles to the dropdown suggested next action
|
||||
*/
|
||||
readonly className?: string
|
||||
}
|
||||
|
||||
interface IDropdownSuggestedActionState<T extends string> {
|
||||
readonly selectedAction: IDropdownSuggestedActionOption<T>
|
||||
}
|
||||
|
||||
export class DropdownSuggestedAction<T extends string> extends React.Component<
|
||||
IDropdownSuggestedActionProps<T>,
|
||||
IDropdownSuggestedActionState<T>
|
||||
> {
|
||||
public constructor(props: IDropdownSuggestedActionProps<T>) {
|
||||
super(props)
|
||||
|
||||
const { selectedActionValue, suggestedActions } = props
|
||||
const firstAction = suggestedActions[0]
|
||||
const selectedAction =
|
||||
selectedActionValue !== undefined
|
||||
? suggestedActions.find(
|
||||
a => a.value === this.props.selectedActionValue
|
||||
) ?? firstAction
|
||||
: firstAction
|
||||
this.state = {
|
||||
selectedAction,
|
||||
}
|
||||
}
|
||||
|
||||
private onActionSelectionChange = (
|
||||
option: IDropdownSelectButtonOption<T>
|
||||
) => {
|
||||
const selectedAction = this.props.suggestedActions.find(
|
||||
a => a.value === option.value
|
||||
)
|
||||
|
||||
if (selectedAction === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({ selectedAction })
|
||||
this.props.onSuggestedActionChanged(selectedAction.value)
|
||||
}
|
||||
|
||||
private onActionSubmitted = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const { onClick, menuItemId } = this.state.selectedAction
|
||||
onClick?.(e)
|
||||
|
||||
if (!e.defaultPrevented && menuItemId !== undefined) {
|
||||
executeMenuItemById(menuItemId)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { selectedAction } = this.state
|
||||
if (selectedAction === undefined) {
|
||||
// Shouldn't happen .. but if it did we don't want to crash app and tell dev what is up
|
||||
sendNonFatalException(
|
||||
'NoSuggestedActionsProvided',
|
||||
new Error(
|
||||
'The DropdownSuggestedActions component was provided an empty array. It requires an array of at least one item.'
|
||||
)
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const {
|
||||
description,
|
||||
image,
|
||||
discoverabilityContent,
|
||||
disabled,
|
||||
value,
|
||||
title,
|
||||
} = selectedAction
|
||||
|
||||
const className = classNames(
|
||||
'suggested-action',
|
||||
'primary',
|
||||
this.props.className
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{image && <div className="image-wrapper">{image}</div>}
|
||||
<div className="text-wrapper">
|
||||
<h2>{title}</h2>
|
||||
{description && <p className="description">{description}</p>}
|
||||
{discoverabilityContent && (
|
||||
<p className="discoverability">{discoverabilityContent}</p>
|
||||
)}
|
||||
</div>
|
||||
<DropdownSelectButton<T>
|
||||
selectedValue={value}
|
||||
options={this.props.suggestedActions.map(({ label, value }) => ({
|
||||
label,
|
||||
value,
|
||||
}))}
|
||||
disabled={disabled}
|
||||
onSelectChange={this.onActionSelectionChange}
|
||||
onSubmit={this.onActionSubmitted}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -99,4 +99,7 @@
|
|||
@import 'ui/_pull-request-quick-view';
|
||||
@import 'ui/discard-changes-retry';
|
||||
@import 'ui/_git-email-not-found-warning';
|
||||
@import 'ui/_branch-select.scss';
|
||||
@import 'ui/_branch-select';
|
||||
@import 'ui/_popover-dropdown';
|
||||
@import 'ui/_pull-request-files-changed';
|
||||
@import 'ui/_pull-request-merge-status';
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
.branch-select-component {
|
||||
display: inline-flex;
|
||||
|
||||
.base-label {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-secondary-color);
|
||||
margin: 0 var(--spacing-half);
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-style: normal;
|
||||
font-family: var(--font-family-monospace);
|
||||
|
||||
.ref-component {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
&.button-component {
|
||||
overflow: visible;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
.ref-component {
|
||||
border-color: var(--path-segment-background-focus);
|
||||
box-shadow: 0 0 0 1px var(--path-segment-background-focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ref-component {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.branch-select-dropdown {
|
||||
position: absolute;
|
||||
min-height: 200px;
|
||||
width: 365px;
|
||||
padding: 0;
|
||||
margin-top: 25px;
|
||||
|
||||
.branch-select-dropdown-header {
|
||||
padding: var(--spacing);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
display: flex;
|
||||
border-bottom: var(--base-border);
|
||||
|
||||
.close {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.branch-select-dropdown-list {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,12 +21,16 @@
|
|||
@import 'dialogs/ci-check-run-rerun';
|
||||
@import 'dialogs/unreachable-commits';
|
||||
@import 'dialogs/open-pull-request';
|
||||
@import 'dialogs/installing-update';
|
||||
|
||||
// The styles herein attempt to follow a flow where margins are only applied
|
||||
// to the bottom of elements (with the exception of the last child). This to
|
||||
// allow easy layout using generalized components and elements such as <Row>
|
||||
// and <p>.
|
||||
dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
overflow: unset;
|
||||
// These are the 24px versions of the alert and stop octicons
|
||||
// from oction v10.0.0
|
||||
|
@ -125,14 +129,28 @@ dialog {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
&:not([open]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// The dialog embeds a fieldset as the first child of the form element
|
||||
// in order to be able to disable all form elements and buttons in one
|
||||
// swoop. This resets all styles for that fieldset.
|
||||
& > form > fieldset {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
& > form {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
& > fieldset {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
.dropdown-select-button {
|
||||
position: relative;
|
||||
|
||||
.dropdown-button-wrappers {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.open-bottom {
|
||||
.invoke-button {
|
||||
border-bottom-left-radius: 0;
|
||||
|
@ -34,19 +38,16 @@
|
|||
}
|
||||
|
||||
.invoke-button {
|
||||
width: 88%;
|
||||
display: inline;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
margin-right: 0;
|
||||
float: left;
|
||||
height: 30px;
|
||||
// counter balances center for the 12% dropdown button
|
||||
padding-left: 12% !important;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
width: 12%;
|
||||
min-width: 30px;
|
||||
padding: var(--spacing-half);
|
||||
margin: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
@ -85,29 +86,50 @@
|
|||
box-shadow: var(--base-box-shadow);
|
||||
width: 99.9%;
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
padding: var(--spacing) var(--spacing-double);
|
||||
padding-left: var(--spacing-triple);
|
||||
border-bottom: 1px solid var(--box-border-color);
|
||||
:first-child.button-component {
|
||||
border-top: 1px solid var(--box-border-color);
|
||||
}
|
||||
|
||||
.button-component {
|
||||
padding: var(--spacing) var(--spacing-double);
|
||||
padding-left: var(--spacing-triple);
|
||||
border-bottom: 1px solid var(--box-border-color);
|
||||
height: auto;
|
||||
padding: var(--spacing) var(--spacing-double);
|
||||
padding-left: var(--spacing-triple);
|
||||
border-bottom: 1px solid var(--box-border-color);
|
||||
text-align: inherit;
|
||||
white-space: normal;
|
||||
border-radius: 0;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
border-top: 0;
|
||||
width: 100%;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline: none;
|
||||
color: var(--box-selected-active-text-color);
|
||||
background-color: var(--box-selected-active-background-color);
|
||||
|
||||
.option-description {
|
||||
color: var(--text-secondary-color);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.selected-option-indicator {
|
||||
position: absolute;
|
||||
left: var(--spacing);
|
||||
color: var(--box-selected-active-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: var(--box-selected-background-color);
|
||||
.option-description {
|
||||
color: var(--text-secondary-color);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.selected-option-indicator {
|
||||
position: absolute;
|
||||
left: var(--spacing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
padding: var(--spacing);
|
||||
|
||||
|
|
36
app/styles/ui/_popover-dropdown.scss
Normal file
36
app/styles/ui/_popover-dropdown.scss
Normal file
|
@ -0,0 +1,36 @@
|
|||
.popover-dropdown-component {
|
||||
display: inline-flex;
|
||||
|
||||
.button-content,
|
||||
.popover-dropdown-button-label {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.popover-dropdown-button-label {
|
||||
color: var(--text-secondary-color);
|
||||
margin: 0 var(--spacing-half);
|
||||
}
|
||||
|
||||
.popover-dropdown-popover {
|
||||
position: absolute;
|
||||
min-height: 200px;
|
||||
width: 365px;
|
||||
padding: 0;
|
||||
margin-top: 25px;
|
||||
|
||||
.popover-dropdown-header {
|
||||
padding: var(--spacing);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
display: flex;
|
||||
border-bottom: var(--base-border);
|
||||
|
||||
.close {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
29
app/styles/ui/_pull-request-files-changed.scss
Normal file
29
app/styles/ui/_pull-request-files-changed.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
.pull-request-files-changed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
|
||||
border: var(--base-border);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
.files-changed-header {
|
||||
padding: var(--spacing);
|
||||
border-bottom: var(--base-border);
|
||||
display: flex;
|
||||
|
||||
.commits-displayed {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.files-diff-viewer {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
border-right: var(--base-border);
|
||||
}
|
||||
}
|
31
app/styles/ui/_pull-request-merge-status.scss
Normal file
31
app/styles/ui/_pull-request-merge-status.scss
Normal file
|
@ -0,0 +1,31 @@
|
|||
.pull-request-merge-status {
|
||||
flex-grow: 1;
|
||||
color: var(--text-secondary-color);
|
||||
|
||||
.octicon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.pr-merge-status-loading {
|
||||
strong {
|
||||
color: var(--file-warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
.pr-merge-status-invalid,
|
||||
.pr-merge-status-conflicts {
|
||||
strong {
|
||||
color: var(--status-error-color);
|
||||
}
|
||||
}
|
||||
|
||||
.pr-merge-status-clean {
|
||||
strong {
|
||||
color: var(--status-success-color);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -193,6 +193,12 @@
|
|||
background: var(--list-item-selected-active-badge-background-color);
|
||||
color: var(--list-item-selected-active-badge-color);
|
||||
}
|
||||
|
||||
.change-indicator-wrapper {
|
||||
.octicon {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -314,6 +314,8 @@
|
|||
}
|
||||
|
||||
&.unified-diff {
|
||||
--line-gutter-right-border-width: 4px;
|
||||
|
||||
.row {
|
||||
.before,
|
||||
.after {
|
||||
|
@ -323,12 +325,27 @@
|
|||
}
|
||||
|
||||
.hunk-handle {
|
||||
left: 100px;
|
||||
// `left` depends on the line number length at runtime
|
||||
transform: translateX(calc(-50% + var(--line-gutter-right-border-width) / 2));
|
||||
}
|
||||
|
||||
&.hunk-info .line-number {
|
||||
background: var(--diff-hunk-gutter-background-color);
|
||||
border-color: var(--diff-hunk-border-color);
|
||||
&.hunk-info {
|
||||
.line-number {
|
||||
background: var(--diff-hunk-gutter-background-color);
|
||||
border-color: var(--diff-hunk-border-color);
|
||||
}
|
||||
.hunk-expansion-handle {
|
||||
background: var(--diff-hunk-gutter-background-color);
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
border-color: var(--diff-hunk-gutter-background-color);
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
|
||||
&.selectable:hover {
|
||||
border-color: var(--diff-hover-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line-number {
|
||||
|
@ -349,8 +366,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.editable .row .line-number {
|
||||
border-right-width: 4px;
|
||||
&.editable .row {
|
||||
.line-number {
|
||||
border-right-width: var(--line-gutter-right-border-width);
|
||||
}
|
||||
.hunk-expansion-handle {
|
||||
border-right-width: var(--line-gutter-right-border-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,14 @@
|
|||
max-width: 600px;
|
||||
}
|
||||
|
||||
.pull-request-action {
|
||||
.dropdown-select-button {
|
||||
.invoke-button {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Lessen the padding at 1.5x zoom and above **/
|
||||
@media screen and (max-width: 640px) {
|
||||
padding: var(--spacing-double);
|
||||
|
|
7
app/styles/ui/dialogs/_installing-update.scss
Normal file
7
app/styles/ui/dialogs/_installing-update.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
#installing-update {
|
||||
max-width: 400px;
|
||||
|
||||
.updating-message {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,9 @@
|
|||
.open-pull-request {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: calc(100% - var(--spacing-double) * 4);
|
||||
max-height: calc(100% - var(--spacing-double) * 4);
|
||||
|
||||
header.dialog-header {
|
||||
padding-bottom: var(--spacing);
|
||||
|
||||
|
@ -15,4 +20,35 @@
|
|||
padding: var(--spacing-half);
|
||||
}
|
||||
}
|
||||
|
||||
.open-pull-request-content {
|
||||
padding: var(--spacing);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.open-pull-request-message {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-double);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
flex-direction: row;
|
||||
|
||||
.button-group {
|
||||
.octicon {
|
||||
margin-right: var(--spacing-half);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pull-request-merge-status {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,7 +175,7 @@
|
|||
vertical-align: bottom; // For some reason, `bottom` places the text in the middle
|
||||
}
|
||||
|
||||
.sha {
|
||||
.selectable {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ body > .tooltip,
|
|||
max-width: 300px;
|
||||
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
background-color: var(--tooltip-background-color);
|
||||
|
|
|
@ -575,9 +575,38 @@ describe('git/diff', () => {
|
|||
'feature-branch',
|
||||
'irrelevantToTest'
|
||||
)
|
||||
|
||||
expect(changesetData).not.toBeNull()
|
||||
if (changesetData === null) {
|
||||
return
|
||||
}
|
||||
|
||||
expect(changesetData.files).toHaveLength(1)
|
||||
expect(changesetData.files[0].path).toBe('feature.md')
|
||||
})
|
||||
|
||||
it('returns null for unrelated histories', async () => {
|
||||
// create a second branch that's orphaned from our current branch
|
||||
await GitProcess.exec(
|
||||
['checkout', '--orphan', 'orphaned-branch'],
|
||||
repository.path
|
||||
)
|
||||
|
||||
// add a commit to this new branch
|
||||
await GitProcess.exec(
|
||||
['commit', '--allow-empty', '-m', `first commit on gh-pages`],
|
||||
repository.path
|
||||
)
|
||||
|
||||
const changesetData = await getBranchMergeBaseChangedFiles(
|
||||
repository,
|
||||
'master',
|
||||
'feature-branch',
|
||||
'irrelevantToTest'
|
||||
)
|
||||
|
||||
expect(changesetData).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBranchMergeBaseDiff', () => {
|
||||
|
|
318
app/test/unit/popup-manager-test.ts
Normal file
318
app/test/unit/popup-manager-test.ts
Normal file
|
@ -0,0 +1,318 @@
|
|||
import { PopupManager } from '../../src/lib/popup-manager'
|
||||
import { Account } from '../../src/models/account'
|
||||
import { Popup, PopupType } from '../../src/models/popup'
|
||||
|
||||
let mockId = 0
|
||||
jest.mock('../../src/lib/uuid', () => {
|
||||
return { uuid: () => mockId++ }
|
||||
})
|
||||
|
||||
describe('PopupManager', () => {
|
||||
let popupManager = new PopupManager()
|
||||
|
||||
beforeEach(() => {
|
||||
popupManager = new PopupManager()
|
||||
mockId = 0
|
||||
})
|
||||
|
||||
describe('currentPopup', () => {
|
||||
it('returns null when no popups added', () => {
|
||||
const currentPopup = popupManager.currentPopup
|
||||
expect(currentPopup).toBeNull()
|
||||
})
|
||||
|
||||
it('returns last added non-error popup', () => {
|
||||
const popupAbout: Popup = { type: PopupType.About }
|
||||
const popupSignIn: Popup = { type: PopupType.SignIn }
|
||||
popupManager.addPopup(popupAbout)
|
||||
popupManager.addPopup(popupSignIn)
|
||||
|
||||
const currentPopup = popupManager.currentPopup
|
||||
expect(currentPopup).not.toBeNull()
|
||||
expect(currentPopup?.type).toBe(PopupType.SignIn)
|
||||
})
|
||||
|
||||
it('returns last added error popup', () => {
|
||||
const popupAbout: Popup = { type: PopupType.About }
|
||||
const popupSignIn: Popup = { type: PopupType.SignIn }
|
||||
popupManager.addPopup(popupAbout)
|
||||
popupManager.addErrorPopup(new Error('an error'))
|
||||
popupManager.addPopup(popupSignIn)
|
||||
|
||||
const currentPopup = popupManager.currentPopup
|
||||
expect(currentPopup).not.toBeNull()
|
||||
expect(currentPopup?.type).toBe(PopupType.Error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAPopupOpen', () => {
|
||||
it('returns false when no popups added', () => {
|
||||
const isAPopupOpen = popupManager.isAPopupOpen
|
||||
expect(isAPopupOpen).toBeFalse()
|
||||
})
|
||||
|
||||
it('returns last added popup', () => {
|
||||
const popupAbout: Popup = { type: PopupType.About }
|
||||
popupManager.addPopup(popupAbout)
|
||||
|
||||
const isAPopupOpen = popupManager.isAPopupOpen
|
||||
expect(isAPopupOpen).toBeTrue()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPopupsOfType', () => {
|
||||
it('returns popups of a given type', () => {
|
||||
const popupAbout: Popup = { type: PopupType.About }
|
||||
const popupSignIn: Popup = { type: PopupType.SignIn }
|
||||
popupManager.addPopup(popupAbout)
|
||||
popupManager.addPopup(popupSignIn)
|
||||
|
||||
const aboutPopups = popupManager.getPopupsOfType(PopupType.About)
|
||||
expect(aboutPopups).toBeArrayOfSize(1)
|
||||
expect(aboutPopups.at(0)?.type).toBe(PopupType.About)
|
||||
})
|
||||
|
||||
it('returns empty array if none exist of given type', () => {
|
||||
const popupAbout: Popup = { type: PopupType.About }
|
||||
popupManager.addPopup(popupAbout)
|
||||
|
||||
const signInPopups = popupManager.getPopupsOfType(PopupType.SignIn)
|
||||
expect(signInPopups).toBeArrayOfSize(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('areTherePopupsOfType', () => {
|
||||
it('returns true if popup of type exists', () => {
|
||||
const popupAbout: Popup = { type: PopupType.About }
|
||||
popupManager.addPopup(popupAbout)
|
||||
|
||||
const areThereAboutPopups = popupManager.areTherePopupsOfType(
|
||||
PopupType.About
|
||||
)
|
||||
expect(areThereAboutPopups).toBeTrue()
|
||||
})
|
||||
|
||||
it('returns false if there are no popups of that type', () => {
|
||||
const popupAbout: Popup = { type: PopupType.About }
|
||||
popupManager.addPopup(popupAbout)
|
||||
|
||||
const areThereSignInPopups = popupManager.areTherePopupsOfType(
|
||||
PopupType.SignIn
|
||||
)
|
||||
expect(areThereSignInPopups).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
describe('addPopup', () => {
|
||||
it('adds a popup to the stack', () => {
|
||||
const popup: Popup = { type: PopupType.About }
|
||||
popupManager.addPopup(popup)
|
||||
|
||||
const popupsOfType = popupManager.getPopupsOfType(PopupType.About)
|
||||
const currentPopup = popupManager.currentPopup
|
||||
expect(popupsOfType).toBeArrayOfSize(1)
|
||||
expect(currentPopup).not.toBeNull()
|
||||
expect(currentPopup?.type).toBe(PopupType.About)
|
||||
expect(currentPopup?.id).toBe(0)
|
||||
})
|
||||
|
||||
it('does not add multiple popups of the same kind to the stack', () => {
|
||||
const popup: Popup = { type: PopupType.About }
|
||||
popupManager.addPopup(popup)
|
||||
popupManager.addPopup(popup)
|
||||
|
||||
const popupsOfType = popupManager.getPopupsOfType(PopupType.About)
|
||||
expect(popupsOfType).toBeArrayOfSize(1)
|
||||
})
|
||||
|
||||
it('adds multiple popups of different types', () => {
|
||||
const popupAbout: Popup = { type: PopupType.About }
|
||||
const popupSignIn: Popup = { type: PopupType.SignIn }
|
||||
popupManager.addPopup(popupAbout)
|
||||
popupManager.addPopup(popupSignIn)
|
||||
|
||||
const aboutPopups = popupManager.getPopupsOfType(PopupType.About)
|
||||
const signInPoups = popupManager.getPopupsOfType(PopupType.SignIn)
|
||||
expect(aboutPopups).toBeArrayOfSize(1)
|
||||
expect(signInPoups).toBeArrayOfSize(1)
|
||||
|
||||
expect(aboutPopups.at(0)?.type).toBe(PopupType.About)
|
||||
expect(signInPoups.at(0)?.type).toBe(PopupType.SignIn)
|
||||
})
|
||||
|
||||
it('trims oldest popup when limit is reached', () => {
|
||||
popupManager = new PopupManager(2)
|
||||
const popupAbout: Popup = { type: PopupType.About }
|
||||
const popupSignIn: Popup = { type: PopupType.SignIn }
|
||||
const popupTermsAndConditions: Popup = {
|
||||
type: PopupType.TermsAndConditions,
|
||||
}
|
||||
popupManager.addPopup(popupAbout)
|
||||
popupManager.addPopup(popupSignIn)
|
||||
popupManager.addPopup(popupTermsAndConditions)
|
||||
|
||||
const aboutPopups = popupManager.getPopupsOfType(PopupType.About)
|
||||
const signInPoups = popupManager.getPopupsOfType(PopupType.SignIn)
|
||||
const termsAndConditionsPoups = popupManager.getPopupsOfType(
|
||||
PopupType.TermsAndConditions
|
||||
)
|
||||
expect(aboutPopups).toBeArrayOfSize(0)
|
||||
expect(signInPoups).toBeArrayOfSize(1)
|
||||
expect(termsAndConditionsPoups).toBeArrayOfSize(1)
|
||||
|
||||
expect(signInPoups.at(0)?.type).toBe(PopupType.SignIn)
|
||||
expect(termsAndConditionsPoups.at(0)?.type).toBe(
|
||||
PopupType.TermsAndConditions
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addErrorPopup', () => {
|
||||
it('adds a popup of type error to the stack', () => {
|
||||
popupManager.addErrorPopup(new Error('an error'))
|
||||
|
||||
const popupsOfType = popupManager.getPopupsOfType(PopupType.Error)
|
||||
const currentPopup = popupManager.currentPopup
|
||||
expect(popupsOfType).toBeArrayOfSize(1)
|
||||
expect(currentPopup).not.toBeNull()
|
||||
expect(currentPopup?.type).toBe(PopupType.Error)
|
||||
expect(currentPopup?.id).toBe(0)
|
||||
})
|
||||
|
||||
it('adds multiple popups of type error to the stack', () => {
|
||||
popupManager.addErrorPopup(new Error('an error'))
|
||||
popupManager.addErrorPopup(new Error('an error'))
|
||||
|
||||
const popupsOfType = popupManager.getPopupsOfType(PopupType.Error)
|
||||
expect(popupsOfType).toBeArrayOfSize(2)
|
||||
})
|
||||
|
||||
it('trims oldest popup when limit is reached', () => {
|
||||
const limit = 2
|
||||
popupManager = new PopupManager(limit)
|
||||
popupManager.addErrorPopup(new Error('an error'))
|
||||
popupManager.addErrorPopup(new Error('an error'))
|
||||
popupManager.addErrorPopup(new Error('an error'))
|
||||
popupManager.addErrorPopup(new Error('an error'))
|
||||
|
||||
const errorPopups = popupManager.getPopupsOfType(PopupType.Error)
|
||||
expect(errorPopups).toBeArrayOfSize(limit)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updatePopup', () => {
|
||||
it('updates the given popup', () => {
|
||||
const mockAccount = new Account('test', '', 'deadbeef', [], '', 1, '')
|
||||
const popupTutorial: Popup = {
|
||||
type: PopupType.CreateTutorialRepository,
|
||||
account: mockAccount,
|
||||
}
|
||||
|
||||
const tutorialPopup = popupManager.addPopup(popupTutorial)
|
||||
|
||||
// Just so update spreader notation will work
|
||||
if (tutorialPopup.type !== PopupType.CreateTutorialRepository) {
|
||||
return
|
||||
}
|
||||
|
||||
const updatedPopup: Popup = {
|
||||
...tutorialPopup,
|
||||
progress: {
|
||||
kind: 'generic',
|
||||
value: 5,
|
||||
},
|
||||
}
|
||||
popupManager.updatePopup(updatedPopup)
|
||||
|
||||
const result = popupManager.getPopupsOfType(
|
||||
PopupType.CreateTutorialRepository
|
||||
)
|
||||
expect(result).toBeArrayOfSize(1)
|
||||
const resultingPopup = result.at(0)
|
||||
// Would fail first expect if not
|
||||
if (resultingPopup === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
expect(resultingPopup.type).toBe(PopupType.CreateTutorialRepository)
|
||||
if (resultingPopup.type !== PopupType.CreateTutorialRepository) {
|
||||
return
|
||||
}
|
||||
|
||||
expect(resultingPopup.progress).toBeDefined()
|
||||
expect(resultingPopup.progress?.kind).toBe('generic')
|
||||
expect(resultingPopup.progress?.value).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removePopup', () => {
|
||||
it('deletes popup when give a popup with an id', () => {
|
||||
const popupAbout: Popup = popupManager.addPopup({ type: PopupType.About })
|
||||
popupManager.addPopup({
|
||||
type: PopupType.SignIn,
|
||||
})
|
||||
|
||||
popupManager.removePopup(popupAbout)
|
||||
|
||||
const aboutPopups = popupManager.getPopupsOfType(PopupType.About)
|
||||
expect(aboutPopups).toBeArrayOfSize(0)
|
||||
|
||||
const signInPopups = popupManager.getPopupsOfType(PopupType.SignIn)
|
||||
expect(signInPopups).toBeArrayOfSize(1)
|
||||
})
|
||||
|
||||
it('does not remove popups by type', () => {
|
||||
popupManager.addPopup({ type: PopupType.About })
|
||||
popupManager.addPopup({
|
||||
type: PopupType.SignIn,
|
||||
})
|
||||
|
||||
popupManager.removePopup({ type: PopupType.About })
|
||||
|
||||
const aboutPopups = popupManager.getPopupsOfType(PopupType.About)
|
||||
expect(aboutPopups).toBeArrayOfSize(1)
|
||||
|
||||
const signInPopups = popupManager.getPopupsOfType(PopupType.SignIn)
|
||||
expect(signInPopups).toBeArrayOfSize(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removePopupByType', () => {
|
||||
it('removes the popups of a given type', () => {
|
||||
popupManager.addPopup({ type: PopupType.About })
|
||||
popupManager.addPopup({
|
||||
type: PopupType.SignIn,
|
||||
})
|
||||
|
||||
popupManager.removePopupByType(PopupType.About)
|
||||
|
||||
const aboutPopups = popupManager.getPopupsOfType(PopupType.About)
|
||||
expect(aboutPopups).toBeArrayOfSize(0)
|
||||
|
||||
const signInPopups = popupManager.getPopupsOfType(PopupType.SignIn)
|
||||
expect(signInPopups).toBeArrayOfSize(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removePopupById', () => {
|
||||
it('removes the popup by its id', () => {
|
||||
const popupAbout: Popup = popupManager.addPopup({ type: PopupType.About })
|
||||
popupManager.addPopup({
|
||||
type: PopupType.SignIn,
|
||||
})
|
||||
|
||||
expect(popupAbout.id).toBeDefined()
|
||||
if (popupAbout.id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
popupManager.removePopupById(popupAbout.id)
|
||||
|
||||
const aboutPopups = popupManager.getPopupsOfType(PopupType.About)
|
||||
expect(aboutPopups).toBeArrayOfSize(0)
|
||||
|
||||
const signInPopups = popupManager.getPopupsOfType(PopupType.SignIn)
|
||||
expect(signInPopups).toBeArrayOfSize(1)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -119,9 +119,9 @@ babel-runtime@^6.26.0:
|
|||
regenerator-runtime "^0.11.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
bl@^3.0.0:
|
||||
version "3.0.1"
|
||||
|
@ -131,9 +131,9 @@ bl@^3.0.0:
|
|||
readable-stream "^3.0.1"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
|
||||
integrity sha1-wHshHHyVLsH479Uad+8NHTmQopI=
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
@ -243,7 +243,7 @@ compare-versions@^3.6.0:
|
|||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||
version "1.1.0"
|
||||
|
@ -854,9 +854,9 @@ mimic-response@^3.1.0:
|
|||
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
|
|
|
@ -1,13 +1,66 @@
|
|||
{
|
||||
"releases": {
|
||||
"3.1.4": ["[Improved] Upgrade embedded Git to 2.35.6"],
|
||||
"3.1.4-beta1": [
|
||||
"[Added] Add support for JetBrains Toolbox and JetBrains Fleet editor for Windows - #12912. Thanks @tsvetilian-ty!",
|
||||
"[Added] Add support for Emacs editor for Linux - #15857. Thanks @zipperer!",
|
||||
"[Added] Add Jetbrains PhpStorm and WebStorm Editors for Linux - #15375. Thanks @patinthehat!",
|
||||
"[Improved] Pull request preview dialog only uses remote branches for base branch options - #15768",
|
||||
"[Improved] Upgrade embedded Git to 2.35.6"
|
||||
],
|
||||
"3.1.3": [
|
||||
"[Fixed] Disable reorder, squashing, cherry-picking while an action of this type is in progress. - #15468",
|
||||
"[Fixed] Using the key command of 'Shift' + 'ArrowDown' adds the next commit below the current selection to the selection - #15549",
|
||||
"[Fixed] Close 'Resolve conflicts before Rebase' dialog will not disable menu items - #13081. Thanks @angusdev!",
|
||||
"[Fixed] Fix commit shortcut (Ctrl/Cmd + Enter) while amending a commit - #15445"
|
||||
],
|
||||
"3.1.3-beta4": [
|
||||
"[Fixed] Hide window instead of hiding the app on macOS - #15511. Thanks @angusdev!",
|
||||
"[Fixed] Only left mouse clicks invoke dragging in the commit list - #15313",
|
||||
"[Fixed] Ensure selected list items stay selected when scrolling - #2957",
|
||||
"[Fixed] Stick to one tooltip at a time in the repository list - #15583",
|
||||
"[Fixed] Preview Pull Request opens when there is not a local default branch - #15704",
|
||||
"[Fixed] Preview Pull Request suggested next action available on first app open without interaction - #15703",
|
||||
"[Improved] Ability to copy tag names from the commit list - #15137. Thanks @Shivareddy-Aluri!",
|
||||
"[Improved] Stacked popups remember their state when hidden due to another popup opening - #15668",
|
||||
"[Improved] Create pull request from pull request preview opens to compare against the user's selected base branch - #15706",
|
||||
"[Improved] On Preview Pull Request dialog, submit button closes the dialog - #15695"
|
||||
],
|
||||
"3.1.3-beta3": [
|
||||
"[Fixed] Using the key command of 'Shift' + 'ArrowDown' in the commit list adds the next commit to the current selection - #15536",
|
||||
"[Fixed] Notifications of Pull Request reviews are displayed for forked repositories - #15580",
|
||||
"[Fixed] Notifications when checks of a Pull Request fail are displayed for forked repositories - #15422",
|
||||
"[Improved] User can preview a Pull Request from the suggested next actions. - #15588",
|
||||
"[Improved] The dropdown selection component is keyboard navigable - #15620",
|
||||
"[Improved] 'Preview Pull Request' menu item availability is consistent with other menu items - #15590",
|
||||
"[Improved] The diff view now highlights Arduino's `.ino` files as C++ source - #15555. Thanks @j-f1!",
|
||||
"[Improved] Close repository list after creating or adding repositories - #15508. Thanks @angusdev!",
|
||||
"[Improved] Always show an error message when an update fails - #15530",
|
||||
"[Improved] Popups are stacked. Opening a popup will not discard an existing popup - #15496"
|
||||
],
|
||||
"3.1.3-beta2": [
|
||||
"[Added] Enable menu option to Force-push branches that have diverged - #15211",
|
||||
"[Added] Add menu option to Fetch the current repository at any time - #7805",
|
||||
"[Added] Support VSCodium as an external editor - #15348. Thanks @daniel-ciaglia!",
|
||||
"[Fixed] Prevent closing the GitHub Desktop while it's being updated - #7055, #5197",
|
||||
"[Fixed] Notifications are shown only when they are relevant to the current repository - #15487",
|
||||
"[Fixed] Disable reorder, squashing, cherry-picking while an action of this type is in progress. - #15468",
|
||||
"[Fixed] Fix repository change indicator not visible if selected and in focus - #7651. Thanks @angusdev!",
|
||||
"[Fixed] Close 'Resolve conflicts before Rebase' dialog will not disable menu items - #13081. Thanks @angusdev!",
|
||||
"[Fixed] Tooltips are positioned properly if mouse is not moved - #13636. Thanks @angusdev!",
|
||||
"[Fixed] Fix tooltips of long commit author emails not breaking properly - #15424. Thanks @angusdev!",
|
||||
"[Fixed] Clone repository progress bar no longer hidden by repository list - #11953. Thanks @angusdev!",
|
||||
"[Fixed] Fix commit shortcut (Ctrl/Cmd + Enter) while amending a commit - #15445",
|
||||
"[Improved] Pull request preview dialog width and height is responsive - #15500"
|
||||
],
|
||||
"3.1.3-beta1": ["[Improved] Upgrade embedded Git to 2.35.5"],
|
||||
"3.1.2": ["[Improved] Upgrade embedded Git to 2.35.5"],
|
||||
"3.1.2-beta1": [
|
||||
"[Added] You can preview the changes a pull request from your current branch would make - #11517",
|
||||
"[Fixed] App correctly remembers undo commit prompt setting - #15408",
|
||||
"[Improved] Add support for zooming out at the 67%, 75%, 80% and 90% zoom levels - #15401. Thanks @sathvikrijo!",
|
||||
"[Improved] Add option to disable discard stash confirmation - #15379. Thanks @tsvetilian-ty!"
|
||||
],
|
||||
"3.1.1": [
|
||||
"[Fixed] App correctly remembers undo commit prompt setting - #15408"
|
||||
],
|
||||
|
|
|
@ -19,13 +19,13 @@ versions look similar to the below output:
|
|||
|
||||
```shellsession
|
||||
$ node -v
|
||||
v10.15.4
|
||||
v16.13.0
|
||||
|
||||
$ yarn -v
|
||||
1.15.2
|
||||
1.21.1
|
||||
|
||||
$ python --version
|
||||
Python 2.7.15
|
||||
Python 3.9.x
|
||||
```
|
||||
|
||||
There are also [additional resources](tooling.md) to configure your favorite
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
|
||||
To authenticate against GitLab repositories you will need to create a personal access token.
|
||||
|
||||
1. Go to your GitLab account and select **Settings** in the user profile dropdown.
|
||||
1. Go to your GitLab account and select **Edit Profile** in the user profile dropdown.
|
||||
|
||||
![](https://user-images.githubusercontent.com/721500/54834720-1f468a00-4c97-11e9-9a0f-4c92224064d0.png)
|
||||
![](https://user-images.githubusercontent.com/721500/206245864-025fedb1-88e5-4c58-84dd-0d4b24eff76d.png)
|
||||
|
||||
2. Select **Access tokens**
|
||||
2. In the left sidebar, select **Access tokens**
|
||||
|
||||
3. Under **Add a personal access token** choose a name and set an expiration date for your token.
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ These editors are currently supported:
|
|||
- [Notepad++](https://notepad-plus-plus.org/)
|
||||
- [RStudio](https://rstudio.com/)
|
||||
- [Aptana Studio](http://www.aptana.com/)
|
||||
- [JetBrains Fleet](https://www.jetbrains.com/fleet/)
|
||||
|
||||
These are defined in a list at the top of the file:
|
||||
|
||||
|
@ -203,6 +204,37 @@ location with an interface that doesn't change between updates.
|
|||
Desktop will confirm this file exists on disk before launching - if it's
|
||||
missing or lost it won't let you launch the external editor.
|
||||
|
||||
### Support for JetBrains Toolbox editors
|
||||
|
||||
Now GitHub Desktop support editors installed through JetBrains Toolbox.
|
||||
The technique used to achieve that is using `jetBrainsToolboxScriptName` field
|
||||
to check if, in the default section for scripts in JetBrainsm Toolbox, a script
|
||||
with the corresponding name exists.
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'JetBrains PyCharm',
|
||||
...
|
||||
jetBrainsToolboxScriptName: 'pycharm',
|
||||
},
|
||||
```
|
||||
|
||||
**Note:** Use `jetBrainsToolboxScriptName` field only on the main edition of
|
||||
the product. When JetBrains Toolbox generates the scripts, it doesn't consider the
|
||||
different editions, so when a new product edition is installed, it generates a
|
||||
shell script with the same name that overrides the existing one. So it's
|
||||
impossible to differentiate between the various editions of the same product.
|
||||
|
||||
**Overriding example:**
|
||||
1. Install JetBrains PyCharm Community
|
||||
2. At this point, JetBrains Toolbox will generate a shell script called `pycharm`
|
||||
3. Install JetBrains PyCharm Professional
|
||||
4. JetBrains Toolbox will generate a new script with the same name, `pycharm`
|
||||
and will override the script generated for the community version
|
||||
|
||||
The current method supports only the default generated JetBrains Toolbox shell
|
||||
scripts.
|
||||
|
||||
## macOS
|
||||
|
||||
The source for the editor integration on macOS is found in
|
||||
|
@ -238,6 +270,7 @@ These editors are currently supported:
|
|||
- [Aptana Studio](http://www.aptana.com/)
|
||||
- [Emacs](https://www.gnu.org/software/emacs/)
|
||||
- [Lite XL](https://lite-xl.com/)
|
||||
- [JetBrains Fleet](https://www.jetbrains.com/fleet/)
|
||||
|
||||
These are defined in a list at the top of the file:
|
||||
|
||||
|
@ -307,6 +340,9 @@ These editors are currently supported:
|
|||
- [Neovim](https://neovim.io/)
|
||||
- [Code](https://github.com/elementary/code)
|
||||
- [Lite XL](https://lite-xl.com/)
|
||||
- [JetBrains PHPStorm](https://www.jetbrains.com/phpstorm/)
|
||||
- [JetBrains WebStorm](https://www.jetbrains.com/webstorm/)
|
||||
- [Emacs](https://www.gnu.org/software/emacs/)
|
||||
|
||||
These are defined in a list at the top of the file:
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
"eslint-plugin-json": "^2.1.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "7.26.1",
|
||||
"express": "^4.15.0",
|
||||
"express": "^4.17.3",
|
||||
"fake-indexeddb": "^2.0.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"front-matter": "^2.3.0",
|
||||
|
|
|
@ -102,7 +102,11 @@ function packageWindows() {
|
|||
}
|
||||
|
||||
if (shouldMakeDelta()) {
|
||||
options.remoteReleases = getUpdatesURL()
|
||||
const url = new URL(getUpdatesURL())
|
||||
// Make sure Squirrel.Windows isn't affected by partially or completely
|
||||
// disabled releases.
|
||||
url.searchParams.set('bypassStaggeredRelease', '1')
|
||||
options.remoteReleases = url.toString()
|
||||
}
|
||||
|
||||
if (isAppveyor() || isGitHubActions()) {
|
||||
|
|
329
yarn.lock
329
yarn.lock
|
@ -1811,13 +1811,13 @@ abab@^2.0.3, abab@^2.0.5:
|
|||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
||||
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
|
||||
|
||||
accepts@~1.3.7:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
||||
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
|
||||
accepts@~1.3.8:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
||||
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
|
||||
dependencies:
|
||||
mime-types "~2.1.24"
|
||||
negotiator "0.6.2"
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
acorn-globals@^6.0.0:
|
||||
version "6.0.0"
|
||||
|
@ -2445,11 +2445,6 @@ bcrypt-pbkdf@^1.0.0:
|
|||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
big.js@^3.1.3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
|
||||
integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
|
||||
|
||||
big.js@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
|
@ -2477,21 +2472,21 @@ bluebird@^3.5.5:
|
|||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
|
||||
body-parser@1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
|
||||
body-parser@1.19.2:
|
||||
version "1.19.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e"
|
||||
integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==
|
||||
dependencies:
|
||||
bytes "3.1.0"
|
||||
bytes "3.1.2"
|
||||
content-type "~1.0.4"
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
http-errors "1.7.2"
|
||||
http-errors "1.8.1"
|
||||
iconv-lite "0.4.24"
|
||||
on-finished "~2.3.0"
|
||||
qs "6.7.0"
|
||||
raw-body "2.4.0"
|
||||
type-is "~1.6.17"
|
||||
qs "6.9.7"
|
||||
raw-body "2.4.3"
|
||||
type-is "~1.6.18"
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
@ -2648,10 +2643,10 @@ builtin-modules@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
|
||||
integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
|
||||
|
||||
bytes@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
|
||||
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
|
||||
bytes@3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||
|
||||
cache-base@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
@ -3019,12 +3014,12 @@ console-polyfill@^0.3.0:
|
|||
resolved "https://registry.yarnpkg.com/console-polyfill/-/console-polyfill-0.3.0.tgz#84900902a18c47a5eba932be75fa44d23e8af861"
|
||||
integrity sha512-w+JSDZS7XML43Xnwo2x5O5vxB0ID7T5BdqDtyqT6uiCAX2kZAgcWxNaGqT97tZfSHzfOcvrfsDAodKcJ3UvnXQ==
|
||||
|
||||
content-disposition@0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
|
||||
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
|
||||
content-disposition@0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
|
||||
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
|
||||
dependencies:
|
||||
safe-buffer "5.1.2"
|
||||
safe-buffer "5.2.1"
|
||||
|
||||
content-type@~1.0.4:
|
||||
version "1.0.4"
|
||||
|
@ -3048,10 +3043,10 @@ cookie-signature@1.0.6:
|
|||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
|
||||
|
||||
cookie@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
|
||||
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
|
||||
cookie@0.4.2:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
|
||||
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
|
||||
|
||||
copy-descriptor@^0.1.0:
|
||||
version "0.1.1"
|
||||
|
@ -3210,55 +3205,20 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
|
|||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.3.4:
|
||||
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^3.2.7:
|
||||
debug@^3.1.0, debug@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.0.1:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87"
|
||||
integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
||||
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
|
||||
integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.3.2, debug@^4.3.3:
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
|
||||
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debuglog@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
||||
|
@ -3275,9 +3235,9 @@ decimal.js@^10.2.1:
|
|||
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
|
||||
|
||||
decode-uri-component@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
|
||||
integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
|
||||
|
||||
decompress-response@^3.3.0:
|
||||
version "3.3.0"
|
||||
|
@ -3677,11 +3637,6 @@ emoji-regex@^9.2.2:
|
|||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
emojis-list@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
|
||||
|
||||
emojis-list@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||
|
@ -4346,17 +4301,17 @@ expect@^26.6.2:
|
|||
jest-message-util "^26.6.2"
|
||||
jest-regex-util "^26.0.0"
|
||||
|
||||
express@^4.15.0:
|
||||
version "4.17.0"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.0.tgz#288af62228a73f4c8ea2990ba3b791bb87cd4438"
|
||||
integrity sha512-1Z7/t3Z5ZnBG252gKUPyItc4xdeaA0X934ca2ewckAsVsw9EG71i++ZHZPYnus8g/s5Bty8IMpSVEuRkmwwPRQ==
|
||||
express@^4.17.3:
|
||||
version "4.17.3"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1"
|
||||
integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==
|
||||
dependencies:
|
||||
accepts "~1.3.7"
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.19.0"
|
||||
content-disposition "0.5.3"
|
||||
body-parser "1.19.2"
|
||||
content-disposition "0.5.4"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.4.0"
|
||||
cookie "0.4.2"
|
||||
cookie-signature "1.0.6"
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
|
@ -4370,13 +4325,13 @@ express@^4.15.0:
|
|||
on-finished "~2.3.0"
|
||||
parseurl "~1.3.3"
|
||||
path-to-regexp "0.1.7"
|
||||
proxy-addr "~2.0.5"
|
||||
qs "6.7.0"
|
||||
proxy-addr "~2.0.7"
|
||||
qs "6.9.7"
|
||||
range-parser "~1.2.1"
|
||||
safe-buffer "5.1.2"
|
||||
send "0.17.1"
|
||||
serve-static "1.14.1"
|
||||
setprototypeof "1.1.1"
|
||||
safe-buffer "5.2.1"
|
||||
send "0.17.2"
|
||||
serve-static "1.14.2"
|
||||
setprototypeof "1.2.0"
|
||||
statuses "~1.5.0"
|
||||
type-is "~1.6.18"
|
||||
utils-merge "1.0.1"
|
||||
|
@ -4634,10 +4589,10 @@ form-data@~2.3.2:
|
|||
combined-stream "1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
forwarded@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
||||
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
|
||||
forwarded@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||
|
||||
fragment-cache@^0.2.1:
|
||||
version "0.2.1"
|
||||
|
@ -5227,16 +5182,16 @@ http-cache-semantics@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||
|
||||
http-errors@1.7.2, http-errors@~1.7.2:
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
|
||||
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
|
||||
http-errors@1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
|
||||
integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==
|
||||
dependencies:
|
||||
depd "~1.1.2"
|
||||
inherits "2.0.3"
|
||||
setprototypeof "1.1.1"
|
||||
inherits "2.0.4"
|
||||
setprototypeof "1.2.0"
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-proxy-agent@^4.0.1:
|
||||
version "4.0.1"
|
||||
|
@ -5342,16 +5297,11 @@ inflight@^1.0.4:
|
|||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
inherits@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
ini@^1.3.4:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
|
@ -5381,10 +5331,10 @@ interpret@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0"
|
||||
integrity sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA=
|
||||
|
||||
ipaddr.js@1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
|
||||
integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
|
||||
ipaddr.js@1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||
|
||||
is-accessor-descriptor@^0.1.6:
|
||||
version "0.1.6"
|
||||
|
@ -6459,15 +6409,10 @@ json5@2.x, json5@^2.1.2:
|
|||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
json5@^0.5.0:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
|
||||
integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
|
||||
|
||||
json5@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
|
||||
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
|
||||
dependencies:
|
||||
minimist "^1.2.0"
|
||||
|
||||
|
@ -6691,13 +6636,13 @@ loader-runner@^4.2.0:
|
|||
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
|
||||
|
||||
loader-utils@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
|
||||
integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3"
|
||||
integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==
|
||||
dependencies:
|
||||
big.js "^3.1.3"
|
||||
emojis-list "^2.0.0"
|
||||
json5 "^0.5.0"
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
json5 "^1.0.1"
|
||||
|
||||
loader-utils@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -7031,7 +6976,7 @@ mime-db@~1.36.0:
|
|||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397"
|
||||
integrity sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
mime-types@^2.1.12, mime-types@~2.1.34:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
|
@ -7112,20 +7057,10 @@ minimist@0.0.8:
|
|||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
|
||||
|
||||
minimist@^1.1.1, minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
|
||||
|
||||
minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
||||
|
||||
mixin-deep@^1.2.0:
|
||||
version "1.2.0"
|
||||
|
@ -7164,17 +7099,12 @@ ms@2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
ms@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ms@^2.1.1:
|
||||
ms@2.1.3, ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
@ -7207,10 +7137,10 @@ natural-compare@^1.4.0:
|
|||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||
|
||||
negotiator@0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||
negotiator@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||
|
||||
neo-async@^2.6.2:
|
||||
version "2.6.2"
|
||||
|
@ -7982,13 +7912,13 @@ proto-list@~1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
|
||||
|
||||
proxy-addr@~2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
|
||||
integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==
|
||||
proxy-addr@~2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
||||
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
|
||||
dependencies:
|
||||
forwarded "~0.1.2"
|
||||
ipaddr.js "1.9.0"
|
||||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
prr@~0.0.0:
|
||||
version "0.0.0"
|
||||
|
@ -8030,15 +7960,15 @@ pupa@^2.0.1:
|
|||
dependencies:
|
||||
escape-goat "^2.0.0"
|
||||
|
||||
qs@6.7.0:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
||||
qs@6.9.7:
|
||||
version "6.9.7"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
|
||||
integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==
|
||||
|
||||
qs@~6.5.2:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||
version "6.5.3"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
|
||||
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
|
||||
|
||||
querystring@^0.2.0:
|
||||
version "0.2.0"
|
||||
|
@ -8069,13 +7999,13 @@ range-parser@^1.2.1, range-parser@~1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
||||
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
|
||||
raw-body@2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c"
|
||||
integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==
|
||||
dependencies:
|
||||
bytes "3.1.0"
|
||||
http-errors "1.7.2"
|
||||
bytes "3.1.2"
|
||||
http-errors "1.8.1"
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
|
@ -8531,21 +8461,16 @@ run-parallel@^1.1.9:
|
|||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
safe-buffer@5.1.2, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==
|
||||
|
||||
safe-buffer@~5.2.0:
|
||||
safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
safe-regex@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
|
||||
|
@ -8686,10 +8611,10 @@ semver@^7.3.7:
|
|||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
send@0.17.1:
|
||||
version "0.17.1"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
|
||||
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
|
||||
send@0.17.2:
|
||||
version "0.17.2"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820"
|
||||
integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
|
@ -8698,9 +8623,9 @@ send@0.17.1:
|
|||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
fresh "0.5.2"
|
||||
http-errors "~1.7.2"
|
||||
http-errors "1.8.1"
|
||||
mime "1.6.0"
|
||||
ms "2.1.1"
|
||||
ms "2.1.3"
|
||||
on-finished "~2.3.0"
|
||||
range-parser "~1.2.1"
|
||||
statuses "~1.5.0"
|
||||
|
@ -8719,15 +8644,15 @@ serialize-javascript@^6.0.0:
|
|||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
serve-static@1.14.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
|
||||
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
|
||||
serve-static@1.14.2:
|
||||
version "1.14.2"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa"
|
||||
integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==
|
||||
dependencies:
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
parseurl "~1.3.3"
|
||||
send "0.17.1"
|
||||
send "0.17.2"
|
||||
|
||||
set-blocking@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -8771,10 +8696,10 @@ setimmediate@^1.0.5:
|
|||
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
|
||||
|
||||
setprototypeof@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
||||
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
|
||||
setprototypeof@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
shallow-clone@^3.0.0:
|
||||
version "3.0.1"
|
||||
|
@ -9487,10 +9412,10 @@ to-space-case@^1.0.0:
|
|||
dependencies:
|
||||
to-no-case "^1.0.0"
|
||||
|
||||
toidentifier@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||
toidentifier@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
totalist@^1.0.0:
|
||||
version "1.1.0"
|
||||
|
@ -9681,7 +9606,7 @@ type-fest@^0.8.1:
|
|||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||
|
||||
type-is@~1.6.17, type-is@~1.6.18:
|
||||
type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||
|
|
Loading…
Reference in a new issue