mirror of
https://github.com/desktop/desktop
synced 2024-09-13 21:31:32 +00:00
Merge branch 'development' into releases/3.1.3
This commit is contained in:
commit
ce4fbb259e
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 }}
|
private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Create Release Pull Request
|
- name: Create Release Pull Request
|
||||||
uses: peter-evans/create-pull-request@v4.1.1
|
uses: peter-evans/create-pull-request@v4.2.3
|
||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test')
|
startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test')
|
||||||
with:
|
with:
|
||||||
|
|
19
README.md
19
README.md
|
@ -4,7 +4,17 @@
|
||||||
GitHub app. It is written in [TypeScript](https://www.typescriptlang.org) and
|
GitHub app. It is written in [TypeScript](https://www.typescriptlang.org) and
|
||||||
uses [React](https://reactjs.org/).
|
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?
|
## 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
|
There are several community-supported package managers that can be used to
|
||||||
install GitHub Desktop:
|
install GitHub Desktop:
|
||||||
- Windows users can install using [Chocolatey](https://chocolatey.org/) package manager:
|
- 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`
|
||||||
`c:\> choco install github-desktop`
|
|
||||||
- macOS users can install using [Homebrew](https://brew.sh/) package manager:
|
- macOS users can install using [Homebrew](https://brew.sh/) package manager:
|
||||||
`$ brew install --cask github`
|
`$ 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.
|
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
|
## More Resources
|
||||||
|
|
||||||
See [desktop.github.com](https://desktop.github.com) for more product-oriented
|
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',
|
'.h': 'text/x-c',
|
||||||
'.cpp': 'text/x-c++src',
|
'.cpp': 'text/x-c++src',
|
||||||
'.hpp': 'text/x-c++src',
|
'.hpp': 'text/x-c++src',
|
||||||
|
'.ino': 'text/x-c++src',
|
||||||
'.kt': 'text/x-kotlin',
|
'.kt': 'text/x-kotlin',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1053,11 +1053,14 @@ export class API {
|
||||||
public async fetchCombinedRefStatus(
|
public async fetchCombinedRefStatus(
|
||||||
owner: string,
|
owner: string,
|
||||||
name: string,
|
name: string,
|
||||||
ref: string
|
ref: string,
|
||||||
|
reloadCache: boolean = false
|
||||||
): Promise<IAPIRefStatus | null> {
|
): Promise<IAPIRefStatus | null> {
|
||||||
const safeRef = encodeURIComponent(ref)
|
const safeRef = encodeURIComponent(ref)
|
||||||
const path = `repos/${owner}/${name}/commits/${safeRef}/status?per_page=100`
|
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 {
|
try {
|
||||||
return await parsedResponse<IAPIRefStatus>(response)
|
return await parsedResponse<IAPIRefStatus>(response)
|
||||||
|
@ -1076,7 +1079,8 @@ export class API {
|
||||||
public async fetchRefCheckRuns(
|
public async fetchRefCheckRuns(
|
||||||
owner: string,
|
owner: string,
|
||||||
name: string,
|
name: string,
|
||||||
ref: string
|
ref: string,
|
||||||
|
reloadCache: boolean = false
|
||||||
): Promise<IAPIRefCheckRuns | null> {
|
): Promise<IAPIRefCheckRuns | null> {
|
||||||
const safeRef = encodeURIComponent(ref)
|
const safeRef = encodeURIComponent(ref)
|
||||||
const path = `repos/${owner}/${name}/commits/${safeRef}/check-runs?per_page=100`
|
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',
|
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 {
|
try {
|
||||||
return await parsedResponse<IAPIRefCheckRuns>(response)
|
return await parsedResponse<IAPIRefCheckRuns>(response)
|
||||||
|
|
|
@ -11,7 +11,10 @@ import { IMenu } from '../models/app-menu'
|
||||||
import { IRemote } from '../models/remote'
|
import { IRemote } from '../models/remote'
|
||||||
import { CloneRepositoryTab } from '../models/clone-repository-tab'
|
import { CloneRepositoryTab } from '../models/clone-repository-tab'
|
||||||
import { BranchesTab } from '../models/branches-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 { IAuthor } from '../models/author'
|
||||||
import { MergeTreeResult } from '../models/merge'
|
import { MergeTreeResult } from '../models/merge'
|
||||||
import { ICommitMessage } from '../models/commit-message'
|
import { ICommitMessage } from '../models/commit-message'
|
||||||
|
@ -22,7 +25,6 @@ import {
|
||||||
ICloneProgress,
|
ICloneProgress,
|
||||||
IMultiCommitOperationProgress,
|
IMultiCommitOperationProgress,
|
||||||
} from '../models/progress'
|
} from '../models/progress'
|
||||||
import { Popup } from '../models/popup'
|
|
||||||
|
|
||||||
import { SignInState } from './stores/sign-in-store'
|
import { SignInState } from './stores/sign-in-store'
|
||||||
|
|
||||||
|
@ -47,6 +49,7 @@ import {
|
||||||
MultiCommitOperationStep,
|
MultiCommitOperationStep,
|
||||||
} from '../models/multi-commit-operation'
|
} from '../models/multi-commit-operation'
|
||||||
import { IChangesetData } from './git'
|
import { IChangesetData } from './git'
|
||||||
|
import { Popup } from '../models/popup'
|
||||||
|
|
||||||
export enum SelectionType {
|
export enum SelectionType {
|
||||||
Repository,
|
Repository,
|
||||||
|
@ -116,6 +119,7 @@ export interface IAppState {
|
||||||
readonly showWelcomeFlow: boolean
|
readonly showWelcomeFlow: boolean
|
||||||
readonly focusCommitMessage: boolean
|
readonly focusCommitMessage: boolean
|
||||||
readonly currentPopup: Popup | null
|
readonly currentPopup: Popup | null
|
||||||
|
readonly allPopups: ReadonlyArray<Popup>
|
||||||
readonly currentFoldout: Foldout | null
|
readonly currentFoldout: Foldout | null
|
||||||
readonly currentBanner: Banner | null
|
readonly currentBanner: Banner | null
|
||||||
|
|
||||||
|
@ -145,7 +149,7 @@ export interface IAppState {
|
||||||
*/
|
*/
|
||||||
readonly appMenuState: ReadonlyArray<IMenu>
|
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. */
|
/** Map from the emoji shortcut (e.g., :+1:) to the image's local path. */
|
||||||
readonly emoji: Map<string, string>
|
readonly emoji: Map<string, string>
|
||||||
|
@ -170,6 +174,9 @@ export interface IAppState {
|
||||||
/** The width of the files list in the stash view */
|
/** The width of the files list in the stash view */
|
||||||
readonly stashedFilesWidth: IConstrainedValue
|
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
|
* Used to highlight access keys throughout the app when the
|
||||||
* Alt key is pressed. Only applicable on non-macOS platforms.
|
* Alt key is pressed. Only applicable on non-macOS platforms.
|
||||||
|
@ -194,6 +201,9 @@ export interface IAppState {
|
||||||
/** Whether we should show a confirmation dialog */
|
/** Whether we should show a confirmation dialog */
|
||||||
readonly askForConfirmationOnDiscardChangesPermanently: boolean
|
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? */
|
/** Should the app prompt the user to confirm a force push? */
|
||||||
readonly askForConfirmationOnForcePush: boolean
|
readonly askForConfirmationOnForcePush: boolean
|
||||||
|
|
||||||
|
@ -230,6 +240,9 @@ export interface IAppState {
|
||||||
/** Whether we should hide white space changes in history diff */
|
/** Whether we should hide white space changes in history diff */
|
||||||
readonly hideWhitespaceInHistoryDiff: boolean
|
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 */
|
/** Whether we should show side by side diffs */
|
||||||
readonly showSideBySideDiff: boolean
|
readonly showSideBySideDiff: boolean
|
||||||
|
|
||||||
|
@ -302,6 +315,11 @@ export interface IAppState {
|
||||||
* Whether or not the user enabled high-signal notifications.
|
* Whether or not the user enabled high-signal notifications.
|
||||||
*/
|
*/
|
||||||
readonly notificationsEnabled: boolean
|
readonly notificationsEnabled: boolean
|
||||||
|
|
||||||
|
/** The users last chosen pull request suggested next action. */
|
||||||
|
readonly pullRequestSuggestedNextAction:
|
||||||
|
| PullRequestSuggestedNextAction
|
||||||
|
| undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FoldoutType {
|
export enum FoldoutType {
|
||||||
|
@ -950,7 +968,7 @@ export interface IPullRequestState {
|
||||||
* The base branch of a a pull request - the branch the currently checked out
|
* The base branch of a a pull request - the branch the currently checked out
|
||||||
* branch would merge into
|
* branch would merge into
|
||||||
*/
|
*/
|
||||||
readonly baseBranch: Branch
|
readonly baseBranch: Branch | null
|
||||||
|
|
||||||
/** The SHAs of commits of the pull request */
|
/** The SHAs of commits of the pull request */
|
||||||
readonly commitSHAs: ReadonlyArray<string> | null
|
readonly commitSHAs: ReadonlyArray<string> | null
|
||||||
|
@ -964,5 +982,8 @@ export interface IPullRequestState {
|
||||||
* repositories commit selection where the diff of all commits represents the
|
* repositories commit selection where the diff of all commits represents the
|
||||||
* diff between the latest commit and the earliest commits parent.
|
* 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',
|
name: 'VSCodium',
|
||||||
bundleIdentifiers: ['com.visualstudio.code.oss'],
|
bundleIdentifiers: ['com.visualstudio.code.oss', 'com.vscodium'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sublime Text',
|
name: 'Sublime Text',
|
||||||
|
|
|
@ -62,6 +62,18 @@ const editors: ILinuxExternalEditor[] = [
|
||||||
name: 'Lite XL',
|
name: 'Lite XL',
|
||||||
paths: ['/usr/bin/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> {
|
async function getAvailablePath(paths: string[]): Promise<string | null> {
|
||||||
|
|
|
@ -46,7 +46,7 @@ export function enableWSLDetection(): boolean {
|
||||||
* Should we use the new diff viewer for unified diffs?
|
* Should we use the new diff viewer for unified diffs?
|
||||||
*/
|
*/
|
||||||
export function enableExperimentalDiffViewer(): boolean {
|
export function enableExperimentalDiffViewer(): boolean {
|
||||||
return false
|
return enableBetaFeatures()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,5 +110,15 @@ export function enableSubmoduleDiff(): boolean {
|
||||||
|
|
||||||
/** Should we enable starting pull requests? */
|
/** Should we enable starting pull requests? */
|
||||||
export function enableStartingPullRequests(): boolean {
|
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,
|
baseBranchName: string,
|
||||||
comparisonBranchName: string,
|
comparisonBranchName: string,
|
||||||
latestComparisonBranchCommitRef: string
|
latestComparisonBranchCommitRef: string
|
||||||
): Promise<IChangesetData> {
|
): Promise<IChangesetData | null> {
|
||||||
const baseArgs = [
|
const baseArgs = [
|
||||||
'diff',
|
'diff',
|
||||||
'--merge-base',
|
'--merge-base',
|
||||||
|
@ -268,22 +268,26 @@ export async function getBranchMergeBaseChangedFiles(
|
||||||
'--',
|
'--',
|
||||||
]
|
]
|
||||||
|
|
||||||
const result = await git(
|
|
||||||
baseArgs,
|
|
||||||
repository.path,
|
|
||||||
'getBranchMergeBaseChangedFiles'
|
|
||||||
)
|
|
||||||
|
|
||||||
const mergeBaseCommit = await getMergeBase(
|
const mergeBaseCommit = await getMergeBase(
|
||||||
repository,
|
repository,
|
||||||
baseBranchName,
|
baseBranchName,
|
||||||
comparisonBranchName
|
comparisonBranchName
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (mergeBaseCommit === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await git(
|
||||||
|
baseArgs,
|
||||||
|
repository.path,
|
||||||
|
'getBranchMergeBaseChangedFiles'
|
||||||
|
)
|
||||||
|
|
||||||
return parseRawLogWithNumstat(
|
return parseRawLogWithNumstat(
|
||||||
result.combinedOutput,
|
result.combinedOutput,
|
||||||
`${latestComparisonBranchCommitRef}`,
|
`${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 {
|
interface HTMLDialogElement {
|
||||||
showModal: () => void
|
showModal: () => void
|
||||||
|
close: (returnValue?: string | undefined) => void
|
||||||
|
open: boolean
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Obtain the number of elements of a tuple type
|
* Obtain the number of elements of a tuple type
|
||||||
|
|
|
@ -46,6 +46,8 @@ export type RequestChannels = {
|
||||||
'menu-event': (name: MenuEvent) => void
|
'menu-event': (name: MenuEvent) => void
|
||||||
log: (level: LogLevel, message: string) => void
|
log: (level: LogLevel, message: string) => void
|
||||||
'will-quit': () => void
|
'will-quit': () => void
|
||||||
|
'will-quit-even-if-updating': () => void
|
||||||
|
'cancel-quitting': () => void
|
||||||
'crash-ready': () => void
|
'crash-ready': () => void
|
||||||
'crash-quit': () => void
|
'crash-quit': () => void
|
||||||
'window-state-changed': (windowState: WindowState) => void
|
'window-state-changed': (windowState: WindowState) => void
|
||||||
|
@ -63,6 +65,7 @@ export type RequestChannels = {
|
||||||
blur: () => void
|
blur: () => void
|
||||||
'update-accounts': (accounts: ReadonlyArray<EndpointToken>) => void
|
'update-accounts': (accounts: ReadonlyArray<EndpointToken>) => void
|
||||||
'quit-and-install-updates': () => void
|
'quit-and-install-updates': () => void
|
||||||
|
'quit-app': () => void
|
||||||
'minimize-window': () => void
|
'minimize-window': () => void
|
||||||
'maximize-window': () => void
|
'maximize-window': () => void
|
||||||
'unmaximize-window': () => void
|
'unmaximize-window': () => void
|
||||||
|
@ -77,6 +80,7 @@ export type RequestChannels = {
|
||||||
'focus-window': () => void
|
'focus-window': () => void
|
||||||
'notification-event': NotificationCallback<DesktopAliveEvent>
|
'notification-event': NotificationCallback<DesktopAliveEvent>
|
||||||
'set-window-zoom-factor': (zoomFactor: number) => void
|
'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 { AppMenu, MenuItem } from '../models/app-menu'
|
||||||
import { hasConflictedFiles } from './status'
|
import { hasConflictedFiles } from './status'
|
||||||
import { findContributionTargetDefaultBranch } from './branch'
|
import { findContributionTargetDefaultBranch } from './branch'
|
||||||
|
import { enableStartingPullRequests } from './feature-flag'
|
||||||
|
|
||||||
export interface IMenuItemState {
|
export interface IMenuItemState {
|
||||||
readonly enabled?: boolean
|
readonly enabled?: boolean
|
||||||
|
@ -135,6 +136,7 @@ const allMenuIds: ReadonlyArray<MenuIDs> = [
|
||||||
'clone-repository',
|
'clone-repository',
|
||||||
'about',
|
'about',
|
||||||
'create-pull-request',
|
'create-pull-request',
|
||||||
|
...(enableStartingPullRequests() ? ['preview-pull-request' as MenuIDs] : []),
|
||||||
'squash-and-merge-branch',
|
'squash-and-merge-branch',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -291,6 +293,13 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder {
|
||||||
'create-pull-request',
|
'create-pull-request',
|
||||||
isHostedOnGitHub && !branchIsUnborn && !onDetachedHead
|
isHostedOnGitHub && !branchIsUnborn && !onDetachedHead
|
||||||
)
|
)
|
||||||
|
if (enableStartingPullRequests()) {
|
||||||
|
menuStateBuilder.setEnabled(
|
||||||
|
'preview-pull-request',
|
||||||
|
!branchIsUnborn && !onDetachedHead && isHostedOnGitHub
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
menuStateBuilder.setEnabled(
|
menuStateBuilder.setEnabled(
|
||||||
'push',
|
'push',
|
||||||
!branchIsUnborn && !onDetachedHead && !networkActionInProgress
|
!branchIsUnborn && !onDetachedHead && !networkActionInProgress
|
||||||
|
@ -330,7 +339,9 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder {
|
||||||
|
|
||||||
menuStateBuilder.disable('view-repository-on-github')
|
menuStateBuilder.disable('view-repository-on-github')
|
||||||
menuStateBuilder.disable('create-pull-request')
|
menuStateBuilder.disable('create-pull-request')
|
||||||
|
if (enableStartingPullRequests()) {
|
||||||
|
menuStateBuilder.disable('preview-pull-request')
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
selectedState &&
|
selectedState &&
|
||||||
selectedState.type === SelectionType.MissingRepository
|
selectedState.type === SelectionType.MissingRepository
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
conflictSteps,
|
conflictSteps,
|
||||||
MultiCommitOperationStepKind,
|
MultiCommitOperationStepKind,
|
||||||
} from '../models/multi-commit-operation'
|
} from '../models/multi-commit-operation'
|
||||||
import { Popup, PopupType } from '../models/popup'
|
|
||||||
import { TipState } from '../models/tip'
|
import { TipState } from '../models/tip'
|
||||||
import { IMultiCommitOperationState, IRepositoryState } from './app-state'
|
import { IMultiCommitOperationState, IRepositoryState } from './app-state'
|
||||||
|
|
||||||
|
@ -39,12 +38,11 @@ export function getMultiCommitOperationChooseBranchStep(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isConflictsFlow(
|
export function isConflictsFlow(
|
||||||
currentPopup: Popup | null,
|
isMultiCommitOperationPopupOpen: boolean,
|
||||||
multiCommitOperationState: IMultiCommitOperationState | null
|
multiCommitOperationState: IMultiCommitOperationState | null
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return (
|
||||||
currentPopup !== null &&
|
isMultiCommitOperationPopupOpen &&
|
||||||
currentPopup.type === PopupType.MultiCommitOperation &&
|
|
||||||
multiCommitOperationState !== null &&
|
multiCommitOperationState !== null &&
|
||||||
conflictSteps.includes(multiCommitOperationState.step.kind)
|
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 { TipState } from '../models/tip'
|
||||||
import { clamp } from './clamp'
|
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
|
* 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
|
* 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
|
* Check application state to see whether the action applied to the current
|
||||||
* branch should be a force push
|
* branch should be a force push
|
||||||
*/
|
*/
|
||||||
export function isCurrentBranchForcePush(
|
export function getCurrentBranchForcePushState(
|
||||||
branchesState: IBranchesState,
|
branchesState: IBranchesState,
|
||||||
aheadBehind: IAheadBehind | null
|
aheadBehind: IAheadBehind | null
|
||||||
) {
|
): ForcePushBranchState {
|
||||||
if (aheadBehind === null) {
|
if (aheadBehind === null) {
|
||||||
// no tracking branch found
|
// 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 { tip, forcePushBranches } = branchesState
|
||||||
const { ahead, behind } = aheadBehind
|
|
||||||
|
|
||||||
let canForcePushBranch = false
|
let canForcePushBranch = false
|
||||||
if (tip.kind === TipState.Valid) {
|
if (tip.kind === TipState.Valid) {
|
||||||
|
@ -36,5 +62,7 @@ export function isCurrentBranchForcePush(
|
||||||
canForcePushBranch = foundEntry === sha
|
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 */
|
/** The number of times the user committed a conflicted merge outside the merge conflicts dialog */
|
||||||
readonly unguidedConflictedMergeCompletionCount: number
|
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
|
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 */
|
/** The number of times the rebase conflicts dialog is dismissed */
|
||||||
readonly rebaseConflictsDialogDismissalCount: number
|
readonly rebaseConflictsDialogDismissalCount: number
|
||||||
|
|
||||||
|
@ -467,6 +474,18 @@ export interface IDailyMeasures {
|
||||||
/** The number of "checks failed" notifications the user received */
|
/** The number of "checks failed" notifications the user received */
|
||||||
readonly checksFailedNotificationCount: number
|
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 */
|
/** The number of "checks failed" notifications the user clicked */
|
||||||
readonly checksFailedNotificationClicked: number
|
readonly checksFailedNotificationClicked: number
|
||||||
|
|
||||||
|
@ -485,6 +504,18 @@ export interface IDailyMeasures {
|
||||||
*/
|
*/
|
||||||
readonly checksFailedDialogRerunChecksCount: number
|
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 */
|
/** The number of "approved PR" notifications the user received */
|
||||||
readonly pullRequestReviewApprovedNotificationCount: number
|
readonly pullRequestReviewApprovedNotificationCount: number
|
||||||
|
|
||||||
|
@ -541,6 +572,9 @@ export interface IDailyMeasures {
|
||||||
|
|
||||||
/** The number of times the user opens a submodule repository from its diff */
|
/** The number of times the user opens a submodule repository from its diff */
|
||||||
readonly openSubmoduleFromDiffCount: number
|
readonly openSubmoduleFromDiffCount: number
|
||||||
|
|
||||||
|
/** The number of times a user has opened the preview pull request dialog */
|
||||||
|
readonly previewedPullRequestCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StatsDatabase extends Dexie {
|
export class StatsDatabase extends Dexie {
|
||||||
|
|
|
@ -112,6 +112,7 @@ const DefaultDailyMeasures: IDailyMeasures = {
|
||||||
guidedConflictedMergeCompletionCount: 0,
|
guidedConflictedMergeCompletionCount: 0,
|
||||||
unguidedConflictedMergeCompletionCount: 0,
|
unguidedConflictedMergeCompletionCount: 0,
|
||||||
createPullRequestCount: 0,
|
createPullRequestCount: 0,
|
||||||
|
createPullRequestFromPreviewCount: 0,
|
||||||
rebaseConflictsDialogDismissalCount: 0,
|
rebaseConflictsDialogDismissalCount: 0,
|
||||||
rebaseConflictsDialogReopenedCount: 0,
|
rebaseConflictsDialogReopenedCount: 0,
|
||||||
rebaseAbortedAfterConflictsCount: 0,
|
rebaseAbortedAfterConflictsCount: 0,
|
||||||
|
@ -195,10 +196,14 @@ const DefaultDailyMeasures: IDailyMeasures = {
|
||||||
viewsCheckJobStepOnline: 0,
|
viewsCheckJobStepOnline: 0,
|
||||||
rerunsChecks: 0,
|
rerunsChecks: 0,
|
||||||
checksFailedNotificationCount: 0,
|
checksFailedNotificationCount: 0,
|
||||||
|
checksFailedNotificationFromRecentRepoCount: 0,
|
||||||
|
checksFailedNotificationFromNonRecentRepoCount: 0,
|
||||||
checksFailedNotificationClicked: 0,
|
checksFailedNotificationClicked: 0,
|
||||||
checksFailedDialogOpenCount: 0,
|
checksFailedDialogOpenCount: 0,
|
||||||
checksFailedDialogSwitchToPullRequestCount: 0,
|
checksFailedDialogSwitchToPullRequestCount: 0,
|
||||||
checksFailedDialogRerunChecksCount: 0,
|
checksFailedDialogRerunChecksCount: 0,
|
||||||
|
pullRequestReviewNotificationFromRecentRepoCount: 0,
|
||||||
|
pullRequestReviewNotificationFromNonRecentRepoCount: 0,
|
||||||
pullRequestReviewApprovedNotificationCount: 0,
|
pullRequestReviewApprovedNotificationCount: 0,
|
||||||
pullRequestReviewApprovedNotificationClicked: 0,
|
pullRequestReviewApprovedNotificationClicked: 0,
|
||||||
pullRequestReviewApprovedDialogSwitchToPullRequestCount: 0,
|
pullRequestReviewApprovedDialogSwitchToPullRequestCount: 0,
|
||||||
|
@ -215,6 +220,7 @@ const DefaultDailyMeasures: IDailyMeasures = {
|
||||||
submoduleDiffViewedFromChangesListCount: 0,
|
submoduleDiffViewedFromChangesListCount: 0,
|
||||||
submoduleDiffViewedFromHistoryCount: 0,
|
submoduleDiffViewedFromHistoryCount: 0,
|
||||||
openSubmoduleFromDiffCount: 0,
|
openSubmoduleFromDiffCount: 0,
|
||||||
|
previewedPullRequestCount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IOnboardingStats {
|
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
|
* 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> {
|
public recordChecksFailedNotificationClicked(): Promise<void> {
|
||||||
return this.updateDailyMeasures(m => ({
|
return this.updateDailyMeasures(m => ({
|
||||||
checksFailedNotificationClicked: m.checksFailedNotificationClicked + 1,
|
checksFailedNotificationClicked: m.checksFailedNotificationClicked + 1,
|
||||||
|
@ -1845,6 +1875,20 @@ export class StatsStore implements IStatsStore {
|
||||||
return `pullRequestReview${infixMap[reviewType]}${suffix}`
|
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.
|
// Generic method to record stats related to Pull Request review notifications.
|
||||||
private recordPullRequestReviewStat(
|
private recordPullRequestReviewStat(
|
||||||
reviewType: ValidNotificationPullRequestReviewState,
|
reviewType: ValidNotificationPullRequestReviewState,
|
||||||
|
@ -1949,6 +1993,15 @@ export class StatsStore implements IStatsStore {
|
||||||
log.error(`Error reporting opt ${direction}:`, e)
|
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,
|
PullRequestCoordinator,
|
||||||
RepositoriesStore,
|
RepositoriesStore,
|
||||||
SignInStore,
|
SignInStore,
|
||||||
|
UpstreamRemoteName,
|
||||||
} from '.'
|
} from '.'
|
||||||
import { Account } from '../../models/account'
|
import { Account } from '../../models/account'
|
||||||
import { AppMenu, IMenu } from '../../models/app-menu'
|
import { AppMenu, IMenu } from '../../models/app-menu'
|
||||||
|
@ -29,7 +30,11 @@ import {
|
||||||
GitHubRepository,
|
GitHubRepository,
|
||||||
hasWritePermission,
|
hasWritePermission,
|
||||||
} from '../../models/github-repository'
|
} from '../../models/github-repository'
|
||||||
import { PullRequest } from '../../models/pull-request'
|
import {
|
||||||
|
defaultPullRequestSuggestedNextAction,
|
||||||
|
PullRequest,
|
||||||
|
PullRequestSuggestedNextAction,
|
||||||
|
} from '../../models/pull-request'
|
||||||
import {
|
import {
|
||||||
forkPullRequestRemoteName,
|
forkPullRequestRemoteName,
|
||||||
IRemote,
|
IRemote,
|
||||||
|
@ -42,6 +47,7 @@ import {
|
||||||
isRepositoryWithGitHubRepository,
|
isRepositoryWithGitHubRepository,
|
||||||
RepositoryWithGitHubRepository,
|
RepositoryWithGitHubRepository,
|
||||||
getNonForkGitHubRepository,
|
getNonForkGitHubRepository,
|
||||||
|
isForkedRepositoryContributingToParent,
|
||||||
} from '../../models/repository'
|
} from '../../models/repository'
|
||||||
import {
|
import {
|
||||||
CommittedFileChange,
|
CommittedFileChange,
|
||||||
|
@ -77,6 +83,10 @@ import {
|
||||||
updatePreferredAppMenuItemLabels,
|
updatePreferredAppMenuItemLabels,
|
||||||
updateAccounts,
|
updateAccounts,
|
||||||
setWindowZoomFactor,
|
setWindowZoomFactor,
|
||||||
|
onShowInstallingUpdate,
|
||||||
|
sendWillQuitEvenIfUpdatingSync,
|
||||||
|
quitApp,
|
||||||
|
sendCancelQuittingSync,
|
||||||
} from '../../ui/main-process-proxy'
|
} from '../../ui/main-process-proxy'
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
|
@ -180,7 +190,7 @@ import {
|
||||||
matchExistingRepository,
|
matchExistingRepository,
|
||||||
urlMatchesRemote,
|
urlMatchesRemote,
|
||||||
} from '../repository-matching'
|
} from '../repository-matching'
|
||||||
import { isCurrentBranchForcePush } from '../rebase'
|
import { ForcePushBranchState, getCurrentBranchForcePushState } from '../rebase'
|
||||||
import { RetryAction, RetryActionType } from '../../models/retry-actions'
|
import { RetryAction, RetryActionType } from '../../models/retry-actions'
|
||||||
import {
|
import {
|
||||||
Default as DefaultShell,
|
Default as DefaultShell,
|
||||||
|
@ -304,6 +314,7 @@ import { offsetFromNow } from '../offset-from'
|
||||||
import { findContributionTargetDefaultBranch } from '../branch'
|
import { findContributionTargetDefaultBranch } from '../branch'
|
||||||
import { ValidNotificationPullRequestReview } from '../valid-notification-pull-request-review'
|
import { ValidNotificationPullRequestReview } from '../valid-notification-pull-request-review'
|
||||||
import { determineMergeability } from '../git/merge-tree'
|
import { determineMergeability } from '../git/merge-tree'
|
||||||
|
import { PopupManager } from '../popup-manager'
|
||||||
|
|
||||||
const LastSelectedRepositoryIDKey = 'last-selected-repository-id'
|
const LastSelectedRepositoryIDKey = 'last-selected-repository-id'
|
||||||
|
|
||||||
|
@ -323,15 +334,20 @@ const commitSummaryWidthConfigKey: string = 'commit-summary-width'
|
||||||
const defaultStashedFilesWidth: number = 250
|
const defaultStashedFilesWidth: number = 250
|
||||||
const stashedFilesWidthConfigKey: string = 'stashed-files-width'
|
const stashedFilesWidthConfigKey: string = 'stashed-files-width'
|
||||||
|
|
||||||
|
const defaultPullRequestFileListWidth: number = 250
|
||||||
|
const pullRequestFileListConfigKey: string = 'pull-request-files-width'
|
||||||
|
|
||||||
const askToMoveToApplicationsFolderDefault: boolean = true
|
const askToMoveToApplicationsFolderDefault: boolean = true
|
||||||
const confirmRepoRemovalDefault: boolean = true
|
const confirmRepoRemovalDefault: boolean = true
|
||||||
const confirmDiscardChangesDefault: boolean = true
|
const confirmDiscardChangesDefault: boolean = true
|
||||||
const confirmDiscardChangesPermanentlyDefault: boolean = true
|
const confirmDiscardChangesPermanentlyDefault: boolean = true
|
||||||
|
const confirmDiscardStashDefault: boolean = true
|
||||||
const askForConfirmationOnForcePushDefault = true
|
const askForConfirmationOnForcePushDefault = true
|
||||||
const confirmUndoCommitDefault: boolean = true
|
const confirmUndoCommitDefault: boolean = true
|
||||||
const askToMoveToApplicationsFolderKey: string = 'askToMoveToApplicationsFolder'
|
const askToMoveToApplicationsFolderKey: string = 'askToMoveToApplicationsFolder'
|
||||||
const confirmRepoRemovalKey: string = 'confirmRepoRemoval'
|
const confirmRepoRemovalKey: string = 'confirmRepoRemoval'
|
||||||
const confirmDiscardChangesKey: string = 'confirmDiscardChanges'
|
const confirmDiscardChangesKey: string = 'confirmDiscardChanges'
|
||||||
|
const confirmDiscardStashKey: string = 'confirmDiscardStash'
|
||||||
const confirmDiscardChangesPermanentlyKey: string =
|
const confirmDiscardChangesPermanentlyKey: string =
|
||||||
'confirmDiscardChangesPermanentlyKey'
|
'confirmDiscardChangesPermanentlyKey'
|
||||||
const confirmForcePushKey: string = 'confirmForcePush'
|
const confirmForcePushKey: string = 'confirmForcePush'
|
||||||
|
@ -348,6 +364,9 @@ const hideWhitespaceInChangesDiffDefault = false
|
||||||
const hideWhitespaceInChangesDiffKey = 'hide-whitespace-in-changes-diff'
|
const hideWhitespaceInChangesDiffKey = 'hide-whitespace-in-changes-diff'
|
||||||
const hideWhitespaceInHistoryDiffDefault = false
|
const hideWhitespaceInHistoryDiffDefault = false
|
||||||
const hideWhitespaceInHistoryDiffKey = 'hide-whitespace-in-diff'
|
const hideWhitespaceInHistoryDiffKey = 'hide-whitespace-in-diff'
|
||||||
|
const hideWhitespaceInPullRequestDiffDefault = false
|
||||||
|
const hideWhitespaceInPullRequestDiffKey =
|
||||||
|
'hide-whitespace-in-pull-request-diff'
|
||||||
|
|
||||||
const commitSpellcheckEnabledDefault = true
|
const commitSpellcheckEnabledDefault = true
|
||||||
const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled'
|
const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled'
|
||||||
|
@ -370,6 +389,9 @@ const MaxInvalidFoldersToDisplay = 3
|
||||||
|
|
||||||
const lastThankYouKey = 'version-and-users-of-last-thank-you'
|
const lastThankYouKey = 'version-and-users-of-last-thank-you'
|
||||||
const customThemeKey = 'custom-theme-key'
|
const customThemeKey = 'custom-theme-key'
|
||||||
|
const pullRequestSuggestedNextActionKey =
|
||||||
|
'pull-request-suggested-next-action-key'
|
||||||
|
|
||||||
export class AppStore extends TypedBaseStore<IAppState> {
|
export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
private readonly gitStoreCache: GitStoreCache
|
private readonly gitStoreCache: GitStoreCache
|
||||||
|
|
||||||
|
@ -388,10 +410,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
private showWelcomeFlow = false
|
private showWelcomeFlow = false
|
||||||
private focusCommitMessage = false
|
private focusCommitMessage = false
|
||||||
private currentPopup: Popup | null = null
|
|
||||||
private currentFoldout: Foldout | null = null
|
private currentFoldout: Foldout | null = null
|
||||||
private currentBanner: Banner | null = null
|
private currentBanner: Banner | null = null
|
||||||
private errors: ReadonlyArray<Error> = new Array<Error>()
|
|
||||||
private emitQueued = false
|
private emitQueued = false
|
||||||
|
|
||||||
private readonly localRepositoryStateLookup = new Map<
|
private readonly localRepositoryStateLookup = new Map<
|
||||||
|
@ -424,6 +444,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
private sidebarWidth = constrain(defaultSidebarWidth)
|
private sidebarWidth = constrain(defaultSidebarWidth)
|
||||||
private commitSummaryWidth = constrain(defaultCommitSummaryWidth)
|
private commitSummaryWidth = constrain(defaultCommitSummaryWidth)
|
||||||
private stashedFilesWidth = constrain(defaultStashedFilesWidth)
|
private stashedFilesWidth = constrain(defaultStashedFilesWidth)
|
||||||
|
private pullRequestFileListWidth = constrain(defaultPullRequestFileListWidth)
|
||||||
|
|
||||||
private windowState: WindowState | null = null
|
private windowState: WindowState | null = null
|
||||||
private windowZoomFactor: number = 1
|
private windowZoomFactor: number = 1
|
||||||
|
@ -437,6 +458,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
private confirmDiscardChanges: boolean = confirmDiscardChangesDefault
|
private confirmDiscardChanges: boolean = confirmDiscardChangesDefault
|
||||||
private confirmDiscardChangesPermanently: boolean =
|
private confirmDiscardChangesPermanently: boolean =
|
||||||
confirmDiscardChangesPermanentlyDefault
|
confirmDiscardChangesPermanentlyDefault
|
||||||
|
private confirmDiscardStash: boolean = confirmDiscardStashDefault
|
||||||
private askForConfirmationOnForcePush = askForConfirmationOnForcePushDefault
|
private askForConfirmationOnForcePush = askForConfirmationOnForcePushDefault
|
||||||
private confirmUndoCommit: boolean = confirmUndoCommitDefault
|
private confirmUndoCommit: boolean = confirmUndoCommitDefault
|
||||||
private imageDiffType: ImageDiffType = imageDiffTypeDefault
|
private imageDiffType: ImageDiffType = imageDiffTypeDefault
|
||||||
|
@ -444,6 +466,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
hideWhitespaceInChangesDiffDefault
|
hideWhitespaceInChangesDiffDefault
|
||||||
private hideWhitespaceInHistoryDiff: boolean =
|
private hideWhitespaceInHistoryDiff: boolean =
|
||||||
hideWhitespaceInHistoryDiffDefault
|
hideWhitespaceInHistoryDiffDefault
|
||||||
|
private hideWhitespaceInPullRequestDiff: boolean =
|
||||||
|
hideWhitespaceInPullRequestDiffDefault
|
||||||
/** Whether or not the spellchecker is enabled for commit summary and description */
|
/** Whether or not the spellchecker is enabled for commit summary and description */
|
||||||
private commitSpellcheckEnabled: boolean = commitSpellcheckEnabledDefault
|
private commitSpellcheckEnabled: boolean = commitSpellcheckEnabledDefault
|
||||||
private showSideBySideDiff: boolean = ShowSideBySideDiffDefault
|
private showSideBySideDiff: boolean = ShowSideBySideDiffDefault
|
||||||
|
@ -488,6 +512,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
private lastThankYou: ILastThankYou | undefined
|
private lastThankYou: ILastThankYou | undefined
|
||||||
private showCIStatusPopover: boolean = false
|
private showCIStatusPopover: boolean = false
|
||||||
|
|
||||||
|
/** A service for managing the stack of open popups */
|
||||||
|
private popupManager = new PopupManager()
|
||||||
|
|
||||||
|
private pullRequestSuggestedNextAction:
|
||||||
|
| PullRequestSuggestedNextAction
|
||||||
|
| undefined = undefined
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly gitHubUserStore: GitHubUserStore,
|
private readonly gitHubUserStore: GitHubUserStore,
|
||||||
private readonly cloningRepositoriesStore: CloningRepositoriesStore,
|
private readonly cloningRepositoriesStore: CloningRepositoriesStore,
|
||||||
|
@ -570,6 +601,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
this.notificationsStore.onPullRequestReviewSubmitNotification(
|
this.notificationsStore.onPullRequestReviewSubmitNotification(
|
||||||
this.onPullRequestReviewSubmitNotification
|
this.onPullRequestReviewSubmitNotification
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onShowInstallingUpdate(this.onShowInstallingUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeWindowState = async () => {
|
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
|
// 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
|
// 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.
|
// one in favor of the error we're about to show.
|
||||||
if (this.currentPopup !== null) {
|
if (this.popupManager.isAPopupOpen) {
|
||||||
return
|
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 */
|
/** Figure out what step of the tutorial the user needs to do next */
|
||||||
private async updateCurrentTutorialStep(
|
private async updateCurrentTutorialStep(
|
||||||
repository: Repository
|
repository: Repository
|
||||||
|
@ -892,15 +931,17 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
appIsFocused: this.appIsFocused,
|
appIsFocused: this.appIsFocused,
|
||||||
selectedState: this.getSelectedState(),
|
selectedState: this.getSelectedState(),
|
||||||
signInState: this.signInStore.getState(),
|
signInState: this.signInStore.getState(),
|
||||||
currentPopup: this.currentPopup,
|
currentPopup: this.popupManager.currentPopup,
|
||||||
|
allPopups: this.popupManager.allPopups,
|
||||||
currentFoldout: this.currentFoldout,
|
currentFoldout: this.currentFoldout,
|
||||||
errors: this.errors,
|
errorCount: this.popupManager.getPopupsOfType(PopupType.Error).length,
|
||||||
showWelcomeFlow: this.showWelcomeFlow,
|
showWelcomeFlow: this.showWelcomeFlow,
|
||||||
focusCommitMessage: this.focusCommitMessage,
|
focusCommitMessage: this.focusCommitMessage,
|
||||||
emoji: this.emoji,
|
emoji: this.emoji,
|
||||||
sidebarWidth: this.sidebarWidth,
|
sidebarWidth: this.sidebarWidth,
|
||||||
commitSummaryWidth: this.commitSummaryWidth,
|
commitSummaryWidth: this.commitSummaryWidth,
|
||||||
stashedFilesWidth: this.stashedFilesWidth,
|
stashedFilesWidth: this.stashedFilesWidth,
|
||||||
|
pullRequestFilesListWidth: this.pullRequestFileListWidth,
|
||||||
appMenuState: this.appMenu ? this.appMenu.openMenus : [],
|
appMenuState: this.appMenu ? this.appMenu.openMenus : [],
|
||||||
highlightAccessKeys: this.highlightAccessKeys,
|
highlightAccessKeys: this.highlightAccessKeys,
|
||||||
isUpdateAvailableBannerVisible: this.isUpdateAvailableBannerVisible,
|
isUpdateAvailableBannerVisible: this.isUpdateAvailableBannerVisible,
|
||||||
|
@ -913,6 +954,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
askForConfirmationOnDiscardChanges: this.confirmDiscardChanges,
|
askForConfirmationOnDiscardChanges: this.confirmDiscardChanges,
|
||||||
askForConfirmationOnDiscardChangesPermanently:
|
askForConfirmationOnDiscardChangesPermanently:
|
||||||
this.confirmDiscardChangesPermanently,
|
this.confirmDiscardChangesPermanently,
|
||||||
|
askForConfirmationOnDiscardStash: this.confirmDiscardStash,
|
||||||
askForConfirmationOnForcePush: this.askForConfirmationOnForcePush,
|
askForConfirmationOnForcePush: this.askForConfirmationOnForcePush,
|
||||||
askForConfirmationOnUndoCommit: this.confirmUndoCommit,
|
askForConfirmationOnUndoCommit: this.confirmUndoCommit,
|
||||||
uncommittedChangesStrategy: this.uncommittedChangesStrategy,
|
uncommittedChangesStrategy: this.uncommittedChangesStrategy,
|
||||||
|
@ -920,6 +962,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
imageDiffType: this.imageDiffType,
|
imageDiffType: this.imageDiffType,
|
||||||
hideWhitespaceInChangesDiff: this.hideWhitespaceInChangesDiff,
|
hideWhitespaceInChangesDiff: this.hideWhitespaceInChangesDiff,
|
||||||
hideWhitespaceInHistoryDiff: this.hideWhitespaceInHistoryDiff,
|
hideWhitespaceInHistoryDiff: this.hideWhitespaceInHistoryDiff,
|
||||||
|
hideWhitespaceInPullRequestDiff: this.hideWhitespaceInPullRequestDiff,
|
||||||
showSideBySideDiff: this.showSideBySideDiff,
|
showSideBySideDiff: this.showSideBySideDiff,
|
||||||
selectedShell: this.selectedShell,
|
selectedShell: this.selectedShell,
|
||||||
repositoryFilterText: this.repositoryFilterText,
|
repositoryFilterText: this.repositoryFilterText,
|
||||||
|
@ -939,6 +982,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
lastThankYou: this.lastThankYou,
|
lastThankYou: this.lastThankYou,
|
||||||
showCIStatusPopover: this.showCIStatusPopover,
|
showCIStatusPopover: this.showCIStatusPopover,
|
||||||
notificationsEnabled: getNotificationsEnabled(),
|
notificationsEnabled: getNotificationsEnabled(),
|
||||||
|
pullRequestSuggestedNextAction: this.pullRequestSuggestedNextAction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1426,17 +1470,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tip.kind === TipState.Valid && aheadBehind.behind > 0) {
|
if (tip.kind === TipState.Valid && aheadBehind.behind > 0) {
|
||||||
const mergeTreePromise = promiseWithMinimumTimeout(
|
this.currentMergeTreePromise = this.setupMergabilityPromise(
|
||||||
() => determineMergeability(repository, tip.branch, action.branch),
|
repository,
|
||||||
500
|
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 => {
|
.then(mergeStatus => {
|
||||||
this.repositoryStateCache.updateCompareState(repository, () => ({
|
this.repositoryStateCache.updateCompareState(repository, () => ({
|
||||||
mergeStatus,
|
mergeStatus,
|
||||||
|
@ -1444,16 +1482,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
const cleanup = () => {
|
this.currentMergeTreePromise = null
|
||||||
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
|
|
||||||
|
|
||||||
return this.currentMergeTreePromise
|
return this.currentMergeTreePromise
|
||||||
} else {
|
} 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`. */
|
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||||
public _updateCompareForm<K extends keyof ICompareFormUpdate>(
|
public _updateCompareForm<K extends keyof ICompareFormUpdate>(
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
|
@ -1717,6 +1765,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
)
|
)
|
||||||
setNumberArray(RecentRepositoriesKey, slicedRecentRepositories)
|
setNumberArray(RecentRepositoriesKey, slicedRecentRepositories)
|
||||||
this.recentRepositories = slicedRecentRepositories
|
this.recentRepositories = slicedRecentRepositories
|
||||||
|
this.notificationsStore.setRecentRepositories(
|
||||||
|
this.repositories.filter(r => this.recentRepositories.includes(r.id))
|
||||||
|
)
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1951,8 +2002,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
this.stashedFilesWidth = constrain(
|
this.stashedFilesWidth = constrain(
|
||||||
getNumber(stashedFilesWidthConfigKey, defaultStashedFilesWidth)
|
getNumber(stashedFilesWidthConfigKey, defaultStashedFilesWidth)
|
||||||
)
|
)
|
||||||
|
this.pullRequestFileListWidth = constrain(
|
||||||
|
getNumber(pullRequestFileListConfigKey, defaultPullRequestFileListWidth)
|
||||||
|
)
|
||||||
|
|
||||||
this.updateResizableConstraints()
|
this.updateResizableConstraints()
|
||||||
|
// TODO: Initiliaze here for now... maybe move to dialog mounting
|
||||||
|
this.updatePullRequestResizableConstraints()
|
||||||
|
|
||||||
this.askToMoveToApplicationsFolderSetting = getBoolean(
|
this.askToMoveToApplicationsFolderSetting = getBoolean(
|
||||||
askToMoveToApplicationsFolderKey,
|
askToMoveToApplicationsFolderKey,
|
||||||
|
@ -1974,6 +2030,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
confirmDiscardChangesPermanentlyDefault
|
confirmDiscardChangesPermanentlyDefault
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.confirmDiscardStash = getBoolean(
|
||||||
|
confirmDiscardStashKey,
|
||||||
|
confirmDiscardStashDefault
|
||||||
|
)
|
||||||
|
|
||||||
this.askForConfirmationOnForcePush = getBoolean(
|
this.askForConfirmationOnForcePush = getBoolean(
|
||||||
confirmForcePushKey,
|
confirmForcePushKey,
|
||||||
askForConfirmationOnForcePushDefault
|
askForConfirmationOnForcePushDefault
|
||||||
|
@ -2011,6 +2072,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
hideWhitespaceInHistoryDiffKey,
|
hideWhitespaceInHistoryDiffKey,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
this.hideWhitespaceInPullRequestDiff = getBoolean(
|
||||||
|
hideWhitespaceInPullRequestDiffKey,
|
||||||
|
false
|
||||||
|
)
|
||||||
this.commitSpellcheckEnabled = getBoolean(
|
this.commitSpellcheckEnabled = getBoolean(
|
||||||
commitSpellcheckEnabledKey,
|
commitSpellcheckEnabledKey,
|
||||||
commitSpellcheckEnabledDefault
|
commitSpellcheckEnabledDefault
|
||||||
|
@ -2034,6 +2099,12 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
this.lastThankYou = getObject<ILastThankYou>(lastThankYouKey)
|
this.lastThankYou = getObject<ILastThankYou>(lastThankYouKey)
|
||||||
|
|
||||||
|
this.pullRequestSuggestedNextAction =
|
||||||
|
getEnum(
|
||||||
|
pullRequestSuggestedNextActionKey,
|
||||||
|
PullRequestSuggestedNextAction
|
||||||
|
) ?? defaultPullRequestSuggestedNextAction
|
||||||
|
|
||||||
this.emitUpdateNow()
|
this.emitUpdateNow()
|
||||||
|
|
||||||
this.accountsStore.refresh()
|
this.accountsStore.refresh()
|
||||||
|
@ -2077,6 +2148,41 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
this.stashedFilesWidth = constrain(this.stashedFilesWidth, 100, filesMax)
|
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(
|
private updateSelectedExternalEditor(
|
||||||
selectedEditor: string | null
|
selectedEditor: string | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -2163,10 +2269,12 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
?.name ?? undefined
|
?.name ?? undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const isForcePushForCurrentRepository = isCurrentBranchForcePush(
|
// From the menu, we'll offer to force-push whenever it's possible, regardless
|
||||||
branchesState,
|
// of whether or not the user performed any action we know would be followed
|
||||||
aheadBehind
|
// by a force-push.
|
||||||
)
|
const isForcePushForCurrentRepository =
|
||||||
|
getCurrentBranchForcePushState(branchesState, aheadBehind) !==
|
||||||
|
ForcePushBranchState.NotAvailable
|
||||||
|
|
||||||
const isStashedChangesVisible =
|
const isStashedChangesVisible =
|
||||||
changesState.selection.kind === ChangesSelectionKind.Stash
|
changesState.selection.kind === ChangesSelectionKind.Stash
|
||||||
|
@ -2440,7 +2548,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
displayingBanner ||
|
displayingBanner ||
|
||||||
isConflictsFlow(this.currentPopup, multiCommitOperationState)
|
isConflictsFlow(
|
||||||
|
this.popupManager.areTherePopupsOfType(PopupType.MultiCommitOperation),
|
||||||
|
multiCommitOperationState
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -2530,7 +2641,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
const { multiCommitOperationState } = state
|
const { multiCommitOperationState } = state
|
||||||
if (
|
if (
|
||||||
userIsStartingMultiCommitOperation(
|
userIsStartingMultiCommitOperation(
|
||||||
this.currentPopup,
|
this.popupManager.currentPopup,
|
||||||
multiCommitOperationState
|
multiCommitOperationState
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -3412,32 +3523,45 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||||
public async _showPopup(popup: Popup): Promise<void> {
|
public async _showPopup(popup: Popup): Promise<void> {
|
||||||
this._closePopup()
|
|
||||||
|
|
||||||
// Always close the app menu when showing a pop up. This is only
|
// Always close the app menu when showing a pop up. This is only
|
||||||
// applicable on Windows where we draw a custom app menu.
|
// applicable on Windows where we draw a custom app menu.
|
||||||
this._closeFoldout(FoldoutType.AppMenu)
|
this._closeFoldout(FoldoutType.AppMenu)
|
||||||
|
|
||||||
this.currentPopup = popup
|
this.popupManager.addPopup(popup)
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||||
public _closePopup(popupType?: PopupType) {
|
public _closePopup(popupType?: PopupType) {
|
||||||
const currentPopup = this.currentPopup
|
const currentPopup = this.popupManager.currentPopup
|
||||||
if (currentPopup == null) {
|
if (currentPopup === null) {
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopup.type === PopupType.CloneRepository) {
|
this.popupManager.removePopupById(popupId)
|
||||||
this._completeOpenInDesktop(() => Promise.resolve(null))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPopup = null
|
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3859,17 +3983,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||||
public _pushError(error: Error): Promise<void> {
|
public _pushError(error: Error): Promise<void> {
|
||||||
const newErrors = Array.from(this.errors)
|
this.popupManager.addErrorPopup(error)
|
||||||
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.emitUpdate()
|
this.emitUpdate()
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
@ -5193,6 +5307,15 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
return Promise.resolve()
|
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> {
|
public _setConfirmForcePushSetting(value: boolean): Promise<void> {
|
||||||
this.askForConfirmationOnForcePush = value
|
this.askForConfirmationOnForcePush = value
|
||||||
setBoolean(confirmForcePushKey, 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) {
|
public _setShowSideBySideDiff(showSideBySideDiff: boolean) {
|
||||||
if (showSideBySideDiff !== this.showSideBySideDiff) {
|
if (showSideBySideDiff !== this.showSideBySideDiff) {
|
||||||
setShowSideBySideDiff(showSideBySideDiff)
|
setShowSideBySideDiff(showSideBySideDiff)
|
||||||
|
@ -5825,7 +5961,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
await this._openInBrowser(url.toString())
|
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
|
const gitHubRepository = repository.gitHubRepository
|
||||||
if (!gitHubRepository) {
|
if (!gitHubRepository) {
|
||||||
return
|
return
|
||||||
|
@ -5838,24 +5977,28 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const branch = tip.branch
|
const compareBranch = tip.branch
|
||||||
const aheadBehind = state.aheadBehind
|
const aheadBehind = state.aheadBehind
|
||||||
|
|
||||||
if (aheadBehind == null) {
|
if (aheadBehind == null) {
|
||||||
this._showPopup({
|
this._showPopup({
|
||||||
type: PopupType.PushBranchCommits,
|
type: PopupType.PushBranchCommits,
|
||||||
repository,
|
repository,
|
||||||
branch,
|
branch: compareBranch,
|
||||||
})
|
})
|
||||||
} else if (aheadBehind.ahead > 0) {
|
} else if (aheadBehind.ahead > 0) {
|
||||||
this._showPopup({
|
this._showPopup({
|
||||||
type: PopupType.PushBranchCommits,
|
type: PopupType.PushBranchCommits,
|
||||||
repository,
|
repository,
|
||||||
branch,
|
branch: compareBranch,
|
||||||
unPushedCommits: aheadBehind.ahead,
|
unPushedCommits: aheadBehind.ahead,
|
||||||
})
|
})
|
||||||
} else {
|
} 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(
|
public async _openCreatePullRequestInBrowser(
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
branch: Branch
|
compareBranch: Branch,
|
||||||
|
baseBranch?: Branch
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const gitHubRepository = repository.gitHubRepository
|
const gitHubRepository = repository.gitHubRepository
|
||||||
if (!gitHubRepository) {
|
if (!gitHubRepository) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlEncodedBranchName = encodeURIComponent(branch.nameWithoutRemote)
|
const { parent, owner, name, htmlURL } = gitHubRepository
|
||||||
const baseURL = `${gitHubRepository.htmlURL}/pull/new/${urlEncodedBranchName}`
|
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)
|
await this._openInBrowser(baseURL)
|
||||||
|
|
||||||
|
@ -6407,13 +6573,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
path,
|
path,
|
||||||
(title, value, description) => {
|
(title, value, description) => {
|
||||||
if (
|
if (
|
||||||
this.currentPopup !== null &&
|
this.popupManager.currentPopup?.type ===
|
||||||
this.currentPopup.type === PopupType.CreateTutorialRepository
|
PopupType.CreateTutorialRepository
|
||||||
) {
|
) {
|
||||||
this.currentPopup = {
|
this.popupManager.updatePopup({
|
||||||
...this.currentPopup,
|
...this.popupManager.currentPopup,
|
||||||
progress: { kind: 'generic', title, value, description },
|
progress: { kind: 'generic', title, value, description },
|
||||||
}
|
})
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7161,33 +7327,47 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async _startPullRequest(repository: Repository) {
|
public async _startPullRequest(repository: Repository) {
|
||||||
const { branchesState } = this.repositoryStateCache.get(repository)
|
const { tip, defaultBranch } =
|
||||||
const { defaultBranch, tip } = branchesState
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBranch = tip.branch
|
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 gitStore = this.gitStoreCache.get(repository)
|
||||||
|
|
||||||
const pullRequestCommits = await gitStore.getCommitsBetweenBranches(
|
const pullRequestCommits = await gitStore.getCommitsBetweenBranches(
|
||||||
defaultBranch,
|
baseBranch,
|
||||||
currentBranch
|
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.
|
// A user may compare two branches with no changes between them.
|
||||||
const emptyChangeSet = { files: [], linesAdded: 0, linesDeleted: 0 }
|
const emptyChangeSet = { files: [], linesAdded: 0, linesDeleted: 0 }
|
||||||
const changesetData =
|
const changesetData =
|
||||||
commitSHAs.length > 0
|
commitsBetweenBranches.length > 0
|
||||||
? await gitStore.performFailableOperation(() =>
|
? await gitStore.performFailableOperation(() =>
|
||||||
getBranchMergeBaseChangedFiles(
|
getBranchMergeBaseChangedFiles(
|
||||||
repository,
|
repository,
|
||||||
defaultBranch.name,
|
baseBranch.name,
|
||||||
currentBranch.name,
|
currentBranch.name,
|
||||||
commitSHAs[0]
|
commitsBetweenBranches[0]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: emptyChangeSet
|
: emptyChangeSet
|
||||||
|
@ -7196,25 +7376,113 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
return
|
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, {
|
this.repositoryStateCache.initializePullRequestState(repository, {
|
||||||
baseBranch: defaultBranch,
|
baseBranch,
|
||||||
commitSHAs,
|
commitSHAs,
|
||||||
commitSelection: {
|
commitSelection: {
|
||||||
shas: commitSHAs,
|
shas: commitSHAs,
|
||||||
shasInDiff: commitSHAs,
|
shasInDiff: commitSHAs,
|
||||||
isContiguous: true,
|
isContiguous: true,
|
||||||
changesetData,
|
changesetData: changesetData ?? emptyChangeSet,
|
||||||
file: null,
|
file: null,
|
||||||
diff: 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(
|
await this._changePullRequestFileSelection(
|
||||||
repository,
|
repository,
|
||||||
changesetData.files[0]
|
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(
|
public async _changePullRequestFileSelection(
|
||||||
|
@ -7233,7 +7501,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
const currentBranch = branchesState.tip.branch
|
const currentBranch = branchesState.tip.branch
|
||||||
const { baseBranch, commitSHAs } = pullRequestState
|
const { baseBranch, commitSHAs } = pullRequestState
|
||||||
if (commitSHAs === null) {
|
if (commitSHAs === null || baseBranch === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7244,6 +7512,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
diff: null,
|
diff: null,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
|
|
||||||
if (commitSHAs.length === 0) {
|
if (commitSHAs.length === 0) {
|
||||||
|
@ -7261,7 +7530,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
file,
|
file,
|
||||||
baseBranch.name,
|
baseBranch.name,
|
||||||
currentBranch.name,
|
currentBranch.name,
|
||||||
this.hideWhitespaceInHistoryDiff,
|
this.hideWhitespaceInPullRequestDiff,
|
||||||
commitSHAs[0]
|
commitSHAs[0]
|
||||||
)
|
)
|
||||||
)) ?? null
|
)) ?? null
|
||||||
|
@ -7284,6 +7553,88 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
this.emitUpdate()
|
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,
|
Repository,
|
||||||
isRepositoryWithGitHubRepository,
|
isRepositoryWithGitHubRepository,
|
||||||
RepositoryWithGitHubRepository,
|
RepositoryWithGitHubRepository,
|
||||||
|
isRepositoryWithForkedGitHubRepository,
|
||||||
|
getForkContributionTarget,
|
||||||
} from '../../models/repository'
|
} 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 { API, APICheckConclusion } from '../api'
|
||||||
import {
|
import {
|
||||||
createCombinedCheckFromChecks,
|
createCombinedCheckFromChecks,
|
||||||
|
@ -66,11 +69,13 @@ export function getNotificationsEnabled() {
|
||||||
*/
|
*/
|
||||||
export class NotificationsStore {
|
export class NotificationsStore {
|
||||||
private repository: RepositoryWithGitHubRepository | null = null
|
private repository: RepositoryWithGitHubRepository | null = null
|
||||||
|
private recentRepositories: ReadonlyArray<Repository> = []
|
||||||
private onChecksFailedCallback: OnChecksFailedCallback | null = null
|
private onChecksFailedCallback: OnChecksFailedCallback | null = null
|
||||||
private onPullRequestReviewSubmitCallback: OnPullRequestReviewSubmitCallback | null =
|
private onPullRequestReviewSubmitCallback: OnPullRequestReviewSubmitCallback | null =
|
||||||
null
|
null
|
||||||
private cachedCommits: Map<string, Commit> = new Map()
|
private cachedCommits: Map<string, Commit> = new Map()
|
||||||
private skipCommitShas: Set<string> = new Set()
|
private skipCommitShas: Set<string> = new Set()
|
||||||
|
private skipCheckRuns: Set<number> = new Set()
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountsStore: AccountsStore,
|
private readonly accountsStore: AccountsStore,
|
||||||
|
@ -121,6 +126,15 @@ export class NotificationsStore {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.isValidRepositoryForEvent(repository, event)) {
|
||||||
|
if (this.isRecentRepositoryEvent(event)) {
|
||||||
|
this.statsStore.recordPullRequestReviewNotiificationFromRecentRepo()
|
||||||
|
} else {
|
||||||
|
this.statsStore.recordPullRequestReviewNotiificationFromNonRecentRepo()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const pullRequests = await this.pullRequestCoordinator.getAllPullRequests(
|
const pullRequests = await this.pullRequestCoordinator.getAllPullRequests(
|
||||||
repository
|
repository
|
||||||
)
|
)
|
||||||
|
@ -134,16 +148,17 @@ export class NotificationsStore {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { gitHubRepository } = repository
|
// PR reviews must be retrieved from the repository the PR belongs to
|
||||||
const api = await this.getAPIForRepository(gitHubRepository)
|
const pullsRepository = this.getContributingRepository(repository)
|
||||||
|
const api = await this.getAPIForRepository(pullsRepository)
|
||||||
|
|
||||||
if (api === null) {
|
if (api === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const review = await api.fetchPullRequestReview(
|
const review = await api.fetchPullRequestReview(
|
||||||
gitHubRepository.owner.login,
|
pullsRepository.owner.login,
|
||||||
gitHubRepository.name,
|
pullsRepository.name,
|
||||||
pullRequest.pullRequestNumber.toString(),
|
pullRequest.pullRequestNumber.toString(),
|
||||||
event.review_id
|
event.review_id
|
||||||
)
|
)
|
||||||
|
@ -192,6 +207,15 @@ export class NotificationsStore {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.isValidRepositoryForEvent(repository, event)) {
|
||||||
|
if (this.isRecentRepositoryEvent(event)) {
|
||||||
|
this.statsStore.recordChecksFailedNotificationFromRecentRepo()
|
||||||
|
} else {
|
||||||
|
this.statsStore.recordChecksFailedNotificationFromNonRecentRepo()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const pullRequests = await this.pullRequestCoordinator.getAllPullRequests(
|
const pullRequests = await this.pullRequestCoordinator.getAllPullRequests(
|
||||||
repository
|
repository
|
||||||
)
|
)
|
||||||
|
@ -234,11 +258,29 @@ export class NotificationsStore {
|
||||||
return
|
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) {
|
if (checks === null) {
|
||||||
return
|
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(
|
const numberOfFailedChecks = checks.filter(
|
||||||
check => check.conclusion === APICheckConclusion.Failure
|
check => check.conclusion === APICheckConclusion.Failure
|
||||||
).length
|
).length
|
||||||
|
@ -250,6 +292,12 @@ export class NotificationsStore {
|
||||||
return
|
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 =
|
const pluralChecks =
|
||||||
numberOfFailedChecks === 1 ? 'check was' : 'checks were'
|
numberOfFailedChecks === 1 ? 'check was' : 'checks were'
|
||||||
|
|
||||||
|
@ -283,14 +331,78 @@ export class NotificationsStore {
|
||||||
this.statsStore.recordChecksFailedNotificationShown()
|
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
|
* Makes the store to keep track of the currently selected repository. Only
|
||||||
* notifications for the currently selected repository will be shown.
|
* notifications for the currently selected repository will be shown.
|
||||||
*/
|
*/
|
||||||
public selectRepository(repository: Repository) {
|
public selectRepository(repository: Repository) {
|
||||||
|
if (repository.hash === this.repository?.hash) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.repository = isRepositoryWithGitHubRepository(repository)
|
this.repository = isRepositoryWithGitHubRepository(repository)
|
||||||
? repository
|
? repository
|
||||||
: null
|
: 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) {
|
private async getAccountForRepository(repository: GitHubRepository) {
|
||||||
|
@ -310,22 +422,20 @@ export class NotificationsStore {
|
||||||
return API.fromAccount(account)
|
return API.fromAccount(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getChecksForRef(
|
private async getChecksForRef(repository: GitHubRepository, ref: string) {
|
||||||
repository: RepositoryWithGitHubRepository,
|
const { owner, name } = repository
|
||||||
ref: string
|
|
||||||
) {
|
|
||||||
const { gitHubRepository } = repository
|
|
||||||
const { owner, name } = gitHubRepository
|
|
||||||
|
|
||||||
const api = await this.getAPIForRepository(gitHubRepository)
|
const api = await this.getAPIForRepository(repository)
|
||||||
|
|
||||||
if (api === null) {
|
if (api === null) {
|
||||||
return 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([
|
const [statuses, checkRuns] = await Promise.all([
|
||||||
api.fetchCombinedRefStatus(owner.login, name, ref),
|
api.fetchCombinedRefStatus(owner.login, name, ref, true),
|
||||||
api.fetchRefCheckRuns(owner.login, name, ref),
|
api.fetchRefCheckRuns(owner.login, name, ref, true),
|
||||||
])
|
])
|
||||||
|
|
||||||
const checks = new Array<IRefCheck>()
|
const checks = new Array<IRefCheck>()
|
||||||
|
|
|
@ -286,7 +286,8 @@ export class RepositoryStateCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldState = pullRequestState.commitSelection
|
const oldState = pullRequestState.commitSelection
|
||||||
const commitSelection = merge(oldState, fn(oldState))
|
const commitSelection =
|
||||||
|
oldState === null ? null : merge(oldState, fn(oldState))
|
||||||
this.updatePullRequestState(repository, () => ({
|
this.updatePullRequestState(repository, () => ({
|
||||||
commitSelection,
|
commitSelection,
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
terminateDesktopNotifications,
|
terminateDesktopNotifications,
|
||||||
} from './notifications'
|
} from './notifications'
|
||||||
import { addTrustedIPCSender } from './trusted-ipc-sender'
|
import { addTrustedIPCSender } from './trusted-ipc-sender'
|
||||||
|
import { enablePreventClosingWhileUpdating } from '../lib/feature-flag'
|
||||||
|
|
||||||
export class AppWindow {
|
export class AppWindow {
|
||||||
private window: Electron.BrowserWindow
|
private window: Electron.BrowserWindow
|
||||||
|
@ -33,6 +34,7 @@ export class AppWindow {
|
||||||
|
|
||||||
private _loadTime: number | null = null
|
private _loadTime: number | null = null
|
||||||
private _rendererReadyTime: number | null = null
|
private _rendererReadyTime: number | null = null
|
||||||
|
private isDownloadingUpdate: boolean = false
|
||||||
|
|
||||||
private minWidth = 960
|
private minWidth = 960
|
||||||
private minHeight = 660
|
private minHeight = 660
|
||||||
|
@ -86,6 +88,7 @@ export class AppWindow {
|
||||||
this.shouldMaximizeOnShow = savedWindowState.isMaximized
|
this.shouldMaximizeOnShow = savedWindowState.isMaximized
|
||||||
|
|
||||||
let quitting = false
|
let quitting = false
|
||||||
|
let quittingEvenIfUpdating = false
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
quitting = true
|
quitting = true
|
||||||
})
|
})
|
||||||
|
@ -95,7 +98,40 @@ export class AppWindow {
|
||||||
event.returnValue = true
|
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 => {
|
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
|
// 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
|
// lets us activate quickly and keep all our interesting logic in the
|
||||||
// renderer.
|
// renderer.
|
||||||
|
@ -104,9 +140,9 @@ export class AppWindow {
|
||||||
// https://github.com/desktop/desktop/issues/12838
|
// https://github.com/desktop/desktop/issues/12838
|
||||||
if (this.window.isFullScreen()) {
|
if (this.window.isFullScreen()) {
|
||||||
this.window.setFullScreen(false)
|
this.window.setFullScreen(false)
|
||||||
this.window.once('leave-full-screen', () => app.hide())
|
this.window.once('leave-full-screen', () => this.window.hide())
|
||||||
} else {
|
} else {
|
||||||
app.hide()
|
this.window.hide()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -213,7 +249,7 @@ export class AppWindow {
|
||||||
return !!this.loadTime && !!this.rendererReadyTime
|
return !!this.loadTime && !!this.rendererReadyTime
|
||||||
}
|
}
|
||||||
|
|
||||||
public onClose(fn: () => void) {
|
public onClosed(fn: () => void) {
|
||||||
this.window.on('closed', fn)
|
this.window.on('closed', fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,10 +380,12 @@ export class AppWindow {
|
||||||
|
|
||||||
public setupAutoUpdater() {
|
public setupAutoUpdater() {
|
||||||
autoUpdater.on('error', (error: Error) => {
|
autoUpdater.on('error', (error: Error) => {
|
||||||
|
this.isDownloadingUpdate = false
|
||||||
ipcWebContents.send(this.window.webContents, 'auto-updater-error', error)
|
ipcWebContents.send(this.window.webContents, 'auto-updater-error', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
autoUpdater.on('checking-for-update', () => {
|
autoUpdater.on('checking-for-update', () => {
|
||||||
|
this.isDownloadingUpdate = false
|
||||||
ipcWebContents.send(
|
ipcWebContents.send(
|
||||||
this.window.webContents,
|
this.window.webContents,
|
||||||
'auto-updater-checking-for-update'
|
'auto-updater-checking-for-update'
|
||||||
|
@ -355,6 +393,7 @@ export class AppWindow {
|
||||||
})
|
})
|
||||||
|
|
||||||
autoUpdater.on('update-available', () => {
|
autoUpdater.on('update-available', () => {
|
||||||
|
this.isDownloadingUpdate = true
|
||||||
ipcWebContents.send(
|
ipcWebContents.send(
|
||||||
this.window.webContents,
|
this.window.webContents,
|
||||||
'auto-updater-update-available'
|
'auto-updater-update-available'
|
||||||
|
@ -362,6 +401,7 @@ export class AppWindow {
|
||||||
})
|
})
|
||||||
|
|
||||||
autoUpdater.on('update-not-available', () => {
|
autoUpdater.on('update-not-available', () => {
|
||||||
|
this.isDownloadingUpdate = false
|
||||||
ipcWebContents.send(
|
ipcWebContents.send(
|
||||||
this.window.webContents,
|
this.window.webContents,
|
||||||
'auto-updater-update-not-available'
|
'auto-updater-update-not-available'
|
||||||
|
@ -369,6 +409,7 @@ export class AppWindow {
|
||||||
})
|
})
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', () => {
|
autoUpdater.on('update-downloaded', () => {
|
||||||
|
this.isDownloadingUpdate = false
|
||||||
ipcWebContents.send(
|
ipcWebContents.send(
|
||||||
this.window.webContents,
|
this.window.webContents,
|
||||||
'auto-updater-update-downloaded'
|
'auto-updater-update-downloaded'
|
||||||
|
|
|
@ -490,6 +490,8 @@ app.on('ready', () => {
|
||||||
mainWindow?.quitAndInstallUpdate()
|
mainWindow?.quitAndInstallUpdate()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ipcMain.on('quit-app', () => app.quit())
|
||||||
|
|
||||||
ipcMain.on('minimize-window', () => mainWindow?.minimizeWindow())
|
ipcMain.on('minimize-window', () => mainWindow?.minimizeWindow())
|
||||||
|
|
||||||
ipcMain.on('maximize-window', () => mainWindow?.maximizeWindow())
|
ipcMain.on('maximize-window', () => mainWindow?.maximizeWindow())
|
||||||
|
@ -738,7 +740,7 @@ function createWindow() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onClose(() => {
|
window.onClosed(() => {
|
||||||
mainWindow = null
|
mainWindow = null
|
||||||
if (!__DARWIN__ && !preventQuit) {
|
if (!__DARWIN__ && !preventQuit) {
|
||||||
app.quit()
|
app.quit()
|
||||||
|
|
|
@ -285,6 +285,12 @@ export function buildDefaultMenu({
|
||||||
accelerator: 'CmdOrCtrl+Shift+P',
|
accelerator: 'CmdOrCtrl+Shift+P',
|
||||||
click: emit('pull'),
|
click: emit('pull'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'fetch',
|
||||||
|
label: __DARWIN__ ? 'Fetch' : '&Fetch',
|
||||||
|
accelerator: 'CmdOrCtrl+Shift+T',
|
||||||
|
click: emit('fetch'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: removeRepoLabel,
|
label: removeRepoLabel,
|
||||||
id: 'remove-repository',
|
id: 'remove-repository',
|
||||||
|
@ -428,12 +434,12 @@ export function buildDefaultMenu({
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if (!hasCurrentPullRequest && enableStartingPullRequests()) {
|
if (enableStartingPullRequests()) {
|
||||||
branchSubmenu.push({
|
branchSubmenu.push({
|
||||||
label: __DARWIN__ ? 'Start Pull Request' : 'Start pull request',
|
label: __DARWIN__ ? 'Preview Pull Request' : 'Preview pull request',
|
||||||
id: 'start-pull-request',
|
id: 'preview-pull-request',
|
||||||
accelerator: 'CmdOrCtrl+Alt+P',
|
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',
|
label: 'Pull Request Check Run Failed',
|
||||||
click: emit('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 */
|
/** 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()
|
const ZoomOutFactors = ZoomInFactors.slice().reverse()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,7 @@ export type MenuEvent =
|
||||||
| 'push'
|
| 'push'
|
||||||
| 'force-push'
|
| 'force-push'
|
||||||
| 'pull'
|
| 'pull'
|
||||||
|
| 'fetch'
|
||||||
| 'show-changes'
|
| 'show-changes'
|
||||||
| 'show-history'
|
| 'show-history'
|
||||||
| 'add-local-repository'
|
| 'add-local-repository'
|
||||||
|
@ -42,4 +43,5 @@ export type MenuEvent =
|
||||||
| 'find-text'
|
| 'find-text'
|
||||||
| 'create-issue-in-repository-on-github'
|
| 'create-issue-in-repository-on-github'
|
||||||
| 'pull-request-check-run-failed'
|
| 'pull-request-check-run-failed'
|
||||||
| 'start-pull-request'
|
| 'preview-pull-request'
|
||||||
|
| 'show-app-error'
|
||||||
|
|
|
@ -35,4 +35,4 @@ export type MenuIDs =
|
||||||
| 'compare-to-branch'
|
| 'compare-to-branch'
|
||||||
| 'toggle-stashed-changes'
|
| 'toggle-stashed-changes'
|
||||||
| 'create-issue-in-repository-on-github'
|
| '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 { IStashEntry } from './stash-entry'
|
||||||
import { Account } from '../models/account'
|
import { Account } from '../models/account'
|
||||||
import { Progress } from './progress'
|
import { Progress } from './progress'
|
||||||
import { ITextDiff, DiffSelection } from './diff'
|
import { ITextDiff, DiffSelection, ImageDiffType } from './diff'
|
||||||
import { RepositorySettingsTab } from '../ui/repository-settings/repository-settings'
|
import { RepositorySettingsTab } from '../ui/repository-settings/repository-settings'
|
||||||
import { ICommitMessage } from './commit-message'
|
import { ICommitMessage } from './commit-message'
|
||||||
import { IAuthor } from './author'
|
import { IAuthor } from './author'
|
||||||
|
@ -24,72 +24,81 @@ import { ValidNotificationPullRequestReview } from '../lib/valid-notification-pu
|
||||||
import { UnreachableCommitsTab } from '../ui/history/unreachable-commits-dialog'
|
import { UnreachableCommitsTab } from '../ui/history/unreachable-commits-dialog'
|
||||||
|
|
||||||
export enum PopupType {
|
export enum PopupType {
|
||||||
RenameBranch = 1,
|
RenameBranch = 'RenameBranch',
|
||||||
DeleteBranch,
|
DeleteBranch = 'DeleteBranch',
|
||||||
DeleteRemoteBranch,
|
DeleteRemoteBranch = 'DeleteRemoteBranch',
|
||||||
ConfirmDiscardChanges,
|
ConfirmDiscardChanges = 'ConfirmDiscardChanges',
|
||||||
Preferences,
|
Preferences = 'Preferences',
|
||||||
RepositorySettings,
|
RepositorySettings = 'RepositorySettings',
|
||||||
AddRepository,
|
AddRepository = 'AddRepository',
|
||||||
CreateRepository,
|
CreateRepository = 'CreateRepository',
|
||||||
CloneRepository,
|
CloneRepository = 'CloneRepository',
|
||||||
CreateBranch,
|
CreateBranch = 'CreateBranch',
|
||||||
SignIn,
|
SignIn = 'SignIn',
|
||||||
About,
|
About = 'About',
|
||||||
InstallGit,
|
InstallGit = 'InstallGit',
|
||||||
PublishRepository,
|
PublishRepository = 'PublishRepository',
|
||||||
Acknowledgements,
|
Acknowledgements = 'Acknowledgements',
|
||||||
UntrustedCertificate,
|
UntrustedCertificate = 'UntrustedCertificate',
|
||||||
RemoveRepository,
|
RemoveRepository = 'RemoveRepository',
|
||||||
TermsAndConditions,
|
TermsAndConditions = 'TermsAndConditions',
|
||||||
PushBranchCommits,
|
PushBranchCommits = 'PushBranchCommits',
|
||||||
CLIInstalled,
|
CLIInstalled = 'CLIInstalled',
|
||||||
GenericGitAuthentication,
|
GenericGitAuthentication = 'GenericGitAuthentication',
|
||||||
ExternalEditorFailed,
|
ExternalEditorFailed = 'ExternalEditorFailed',
|
||||||
OpenShellFailed,
|
OpenShellFailed = 'OpenShellFailed',
|
||||||
InitializeLFS,
|
InitializeLFS = 'InitializeLFS',
|
||||||
LFSAttributeMismatch,
|
LFSAttributeMismatch = 'LFSAttributeMismatch',
|
||||||
UpstreamAlreadyExists,
|
UpstreamAlreadyExists = 'UpstreamAlreadyExists',
|
||||||
ReleaseNotes,
|
ReleaseNotes = 'ReleaseNotes',
|
||||||
DeletePullRequest,
|
DeletePullRequest = 'DeletePullRequest',
|
||||||
OversizedFiles,
|
OversizedFiles = 'OversizedFiles',
|
||||||
CommitConflictsWarning,
|
CommitConflictsWarning = 'CommitConflictsWarning',
|
||||||
PushNeedsPull,
|
PushNeedsPull = 'PushNeedsPull',
|
||||||
ConfirmForcePush,
|
ConfirmForcePush = 'ConfirmForcePush',
|
||||||
StashAndSwitchBranch,
|
StashAndSwitchBranch = 'StashAndSwitchBranch',
|
||||||
ConfirmOverwriteStash,
|
ConfirmOverwriteStash = 'ConfirmOverwriteStash',
|
||||||
ConfirmDiscardStash,
|
ConfirmDiscardStash = 'ConfirmDiscardStash',
|
||||||
CreateTutorialRepository,
|
CreateTutorialRepository = 'CreateTutorialRepository',
|
||||||
ConfirmExitTutorial,
|
ConfirmExitTutorial = 'ConfirmExitTutorial',
|
||||||
PushRejectedDueToMissingWorkflowScope,
|
PushRejectedDueToMissingWorkflowScope = 'PushRejectedDueToMissingWorkflowScope',
|
||||||
SAMLReauthRequired,
|
SAMLReauthRequired = 'SAMLReauthRequired',
|
||||||
CreateFork,
|
CreateFork = 'CreateFork',
|
||||||
CreateTag,
|
CreateTag = 'CreateTag',
|
||||||
DeleteTag,
|
DeleteTag = 'DeleteTag',
|
||||||
LocalChangesOverwritten,
|
LocalChangesOverwritten = 'LocalChangesOverwritten',
|
||||||
ChooseForkSettings,
|
ChooseForkSettings = 'ChooseForkSettings',
|
||||||
ConfirmDiscardSelection,
|
ConfirmDiscardSelection = 'ConfirmDiscardSelection',
|
||||||
MoveToApplicationsFolder,
|
MoveToApplicationsFolder = 'MoveToApplicationsFolder',
|
||||||
ChangeRepositoryAlias,
|
ChangeRepositoryAlias = 'ChangeRepositoryAlias',
|
||||||
ThankYou,
|
ThankYou = 'ThankYou',
|
||||||
CommitMessage,
|
CommitMessage = 'CommitMessage',
|
||||||
MultiCommitOperation,
|
MultiCommitOperation = 'MultiCommitOperation',
|
||||||
WarnLocalChangesBeforeUndo,
|
WarnLocalChangesBeforeUndo = 'WarnLocalChangesBeforeUndo',
|
||||||
WarningBeforeReset,
|
WarningBeforeReset = 'WarningBeforeReset',
|
||||||
InvalidatedToken,
|
InvalidatedToken = 'InvalidatedToken',
|
||||||
AddSSHHost,
|
AddSSHHost = 'AddSSHHost',
|
||||||
SSHKeyPassphrase,
|
SSHKeyPassphrase = 'SSHKeyPassphrase',
|
||||||
SSHUserPassword,
|
SSHUserPassword = 'SSHUserPassword',
|
||||||
PullRequestChecksFailed,
|
PullRequestChecksFailed = 'PullRequestChecksFailed',
|
||||||
CICheckRunRerun,
|
CICheckRunRerun = 'CICheckRunRerun',
|
||||||
WarnForcePush,
|
WarnForcePush = 'WarnForcePush',
|
||||||
DiscardChangesRetry,
|
DiscardChangesRetry = 'DiscardChangesRetry',
|
||||||
PullRequestReview,
|
PullRequestReview = 'PullRequestReview',
|
||||||
UnreachableCommits,
|
UnreachableCommits = 'UnreachableCommits',
|
||||||
StartPullRequest,
|
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.RenameBranch; repository: Repository; branch: Branch }
|
||||||
| {
|
| {
|
||||||
type: PopupType.DeleteBranch
|
type: PopupType.DeleteBranch
|
||||||
|
@ -362,4 +371,23 @@ export type Popup =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: PopupType.StartPullRequest
|
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
|
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
|
? repository.workflowPreferences.forkContributionTarget
|
||||||
: ForkContributionTarget.Parent
|
: 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 { assertNever } from '../../lib/fatal-error'
|
||||||
import { ReleaseNotesUri } from '../lib/releases'
|
import { ReleaseNotesUri } from '../lib/releases'
|
||||||
import { encodePathAsUrl } from '../../lib/path'
|
import { encodePathAsUrl } from '../../lib/path'
|
||||||
|
import { isTopMostDialog } from '../dialog/is-top-most'
|
||||||
|
|
||||||
const logoPath = __DARWIN__
|
const logoPath = __DARWIN__
|
||||||
? 'static/logo-64x64@2x.png'
|
? 'static/logo-64x64@2x.png'
|
||||||
|
@ -54,6 +55,9 @@ interface IAboutProps {
|
||||||
|
|
||||||
/** A function to call when the user wants to see Terms and Conditions. */
|
/** A function to call when the user wants to see Terms and Conditions. */
|
||||||
readonly onShowTermsAndConditions: () => void
|
readonly onShowTermsAndConditions: () => void
|
||||||
|
|
||||||
|
/** Whether the dialog is the top most in the dialog stack */
|
||||||
|
readonly isTopMost: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAboutState {
|
interface IAboutState {
|
||||||
|
@ -67,6 +71,16 @@ interface IAboutState {
|
||||||
*/
|
*/
|
||||||
export class About extends React.Component<IAboutProps, IAboutState> {
|
export class About extends React.Component<IAboutProps, IAboutState> {
|
||||||
private updateStoreEventHandle: Disposable | null = null
|
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) {
|
public constructor(props: IAboutProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
@ -86,8 +100,11 @@ export class About extends React.Component<IAboutProps, IAboutState> {
|
||||||
this.onUpdateStateChanged
|
this.onUpdateStateChanged
|
||||||
)
|
)
|
||||||
this.setState({ updateState: updateStore.state })
|
this.setState({ updateState: updateStore.state })
|
||||||
window.addEventListener('keydown', this.onKeyDown)
|
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||||
window.addEventListener('keyup', this.onKeyUp)
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(): void {
|
||||||
|
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -95,8 +112,7 @@ export class About extends React.Component<IAboutProps, IAboutState> {
|
||||||
this.updateStoreEventHandle.dispose()
|
this.updateStoreEventHandle.dispose()
|
||||||
this.updateStoreEventHandle = null
|
this.updateStoreEventHandle = null
|
||||||
}
|
}
|
||||||
window.removeEventListener('keydown', this.onKeyDown)
|
this.checkIsTopMostDialog(false)
|
||||||
window.removeEventListener('keyup', this.onKeyUp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onKeyDown = (event: KeyboardEvent) => {
|
private onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||||
import { LinkButton } from '../lib/link-button'
|
import { LinkButton } from '../lib/link-button'
|
||||||
import { PopupType } from '../../models/popup'
|
import { PopupType } from '../../models/popup'
|
||||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||||
|
import { FoldoutType } from '../../lib/app-state'
|
||||||
|
|
||||||
import untildify from 'untildify'
|
import untildify from 'untildify'
|
||||||
import { showOpenDialog } from '../main-process-proxy'
|
import { showOpenDialog } from '../main-process-proxy'
|
||||||
|
@ -265,6 +266,7 @@ export class AddExistingRepository extends React.Component<
|
||||||
const repositories = await dispatcher.addRepositories([resolvedPath])
|
const repositories = await dispatcher.addRepositories([resolvedPath])
|
||||||
|
|
||||||
if (repositories.length > 0) {
|
if (repositories.length > 0) {
|
||||||
|
dispatcher.closeFoldout(FoldoutType.Repository)
|
||||||
dispatcher.selectRepository(repositories[0])
|
dispatcher.selectRepository(repositories[0])
|
||||||
dispatcher.recordAddExistingRepository()
|
dispatcher.recordAddExistingRepository()
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,9 @@ import { showOpenDialog } from '../main-process-proxy'
|
||||||
import { pathExists } from '../lib/path-exists'
|
import { pathExists } from '../lib/path-exists'
|
||||||
import { mkdir } from 'fs/promises'
|
import { mkdir } from 'fs/promises'
|
||||||
import { directoryExists } from '../../lib/directory-exists'
|
import { directoryExists } from '../../lib/directory-exists'
|
||||||
|
import { FoldoutType } from '../../lib/app-state'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { isTopMostDialog } from '../dialog/is-top-most'
|
||||||
|
|
||||||
/** The sentinel value used to indicate no gitignore should be used. */
|
/** The sentinel value used to indicate no gitignore should be used. */
|
||||||
const NoGitIgnoreValue = 'None'
|
const NoGitIgnoreValue = 'None'
|
||||||
|
@ -70,6 +72,9 @@ interface ICreateRepositoryProps {
|
||||||
|
|
||||||
/** Prefills path input so user doesn't have to. */
|
/** Prefills path input so user doesn't have to. */
|
||||||
readonly initialPath?: string
|
readonly initialPath?: string
|
||||||
|
|
||||||
|
/** Whether the dialog is the top most in the dialog stack */
|
||||||
|
readonly isTopMost: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICreateRepositoryState {
|
interface ICreateRepositoryState {
|
||||||
|
@ -114,6 +119,16 @@ export class CreateRepository extends React.Component<
|
||||||
ICreateRepositoryProps,
|
ICreateRepositoryProps,
|
||||||
ICreateRepositoryState
|
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) {
|
public constructor(props: ICreateRepositoryProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
@ -144,7 +159,7 @@ export class CreateRepository extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
window.addEventListener('focus', this.onWindowFocus)
|
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||||
|
|
||||||
const gitIgnoreNames = await getGitIgnoreNames()
|
const gitIgnoreNames = await getGitIgnoreNames()
|
||||||
const licenses = await getLicenses()
|
const licenses = await getLicenses()
|
||||||
|
@ -157,8 +172,12 @@ export class CreateRepository extends React.Component<
|
||||||
this.updateReadMeExists(path, this.state.name)
|
this.updateReadMeExists(path, this.state.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentDidUpdate(): void {
|
||||||
window.removeEventListener('focus', this.onWindowFocus)
|
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
this.checkIsTopMostDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializePath = async () => {
|
private initializePath = async () => {
|
||||||
|
@ -391,6 +410,7 @@ export class CreateRepository extends React.Component<
|
||||||
|
|
||||||
this.updateDefaultDirectory()
|
this.updateDefaultDirectory()
|
||||||
|
|
||||||
|
this.props.dispatcher.closeFoldout(FoldoutType.Repository)
|
||||||
this.props.dispatcher.selectRepository(repository)
|
this.props.dispatcher.selectRepository(repository)
|
||||||
this.props.dispatcher.recordCreateRepository()
|
this.props.dispatcher.recordCreateRepository()
|
||||||
this.props.onDismissed()
|
this.props.onDismissed()
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
import { dialogTransitionTimeout } from './app'
|
import { dialogTransitionTimeout } from './app'
|
||||||
import { GitError, isAuthFailureError } from '../lib/git/core'
|
import { GitError, isAuthFailureError } from '../lib/git/core'
|
||||||
import { Popup, PopupType } from '../models/popup'
|
import { Popup, PopupType } from '../models/popup'
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
|
||||||
import { OkCancelButtonGroup } from './dialog/ok-cancel-button-group'
|
import { OkCancelButtonGroup } from './dialog/ok-cancel-button-group'
|
||||||
import { ErrorWithMetadata } from '../lib/error-with-metadata'
|
import { ErrorWithMetadata } from '../lib/error-with-metadata'
|
||||||
import { RetryActionType, RetryAction } from '../models/retry-actions'
|
import { RetryActionType, RetryAction } from '../models/retry-actions'
|
||||||
|
@ -18,14 +17,11 @@ import memoizeOne from 'memoize-one'
|
||||||
import { parseCarriageReturn } from '../lib/parse-carriage-return'
|
import { parseCarriageReturn } from '../lib/parse-carriage-return'
|
||||||
|
|
||||||
interface IAppErrorProps {
|
interface IAppErrorProps {
|
||||||
/** The list of queued, app-wide, errors */
|
/** The error to be displayed */
|
||||||
readonly errors: ReadonlyArray<Error>
|
readonly error: Error
|
||||||
|
|
||||||
/**
|
/** Called to dismiss the dialog */
|
||||||
* A callback which is used whenever a particular error
|
readonly onDismissed: () => void
|
||||||
* has been shown to, and been dismissed by, the user.
|
|
||||||
*/
|
|
||||||
readonly onClearError: (error: Error) => void
|
|
||||||
readonly onShowPopup: (popupType: Popup) => void | undefined
|
readonly onShowPopup: (popupType: Popup) => void | undefined
|
||||||
readonly onRetryAction: (retryAction: RetryAction) => void
|
readonly onRetryAction: (retryAction: RetryAction) => void
|
||||||
}
|
}
|
||||||
|
@ -53,13 +49,13 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
||||||
public constructor(props: IAppErrorProps) {
|
public constructor(props: IAppErrorProps) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
error: props.errors[0] || null,
|
error: props.error,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillReceiveProps(nextProps: IAppErrorProps) {
|
public componentWillReceiveProps(nextProps: IAppErrorProps) {
|
||||||
const error = nextProps.errors[0] || null
|
const error = nextProps.error
|
||||||
|
|
||||||
// We keep the currently shown error until it has disappeared
|
// We keep the currently shown error until it has disappeared
|
||||||
// from the first spot in the application error queue.
|
// 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 = () => {
|
private showPreferencesDialog = () => {
|
||||||
this.onDismissed()
|
this.props.onDismissed()
|
||||||
|
|
||||||
//This is a hacky solution to resolve multiple dialog windows
|
//This is a hacky solution to resolve multiple dialog windows
|
||||||
//being open at the same time.
|
//being open at the same time.
|
||||||
|
@ -95,7 +76,7 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
||||||
|
|
||||||
private onRetryAction = (event: React.MouseEvent<HTMLButtonElement>) => {
|
private onRetryAction = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.onDismissed()
|
this.props.onDismissed()
|
||||||
|
|
||||||
const { error } = this.state
|
const { error } = this.state
|
||||||
|
|
||||||
|
@ -128,36 +109,6 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
||||||
return 'Error'
|
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) {
|
private renderContentAfterErrorMessage(error: Error) {
|
||||||
if (!isErrorWithMetaData(error)) {
|
if (!isErrorWithMetaData(error)) {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -207,7 +158,7 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
||||||
|
|
||||||
private onCloseButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
private onCloseButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.onDismissed()
|
this.props.onDismissed()
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderFooter(error: Error) {
|
private renderFooter(error: Error) {
|
||||||
|
@ -257,16 +208,32 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const dialogContent = this.renderDialog()
|
const error = this.state.error
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransitionGroup>
|
<Dialog
|
||||||
{dialogContent && (
|
id="app-error"
|
||||||
<CSSTransition classNames="modal" timeout={dialogTransitionTimeout}>
|
type="error"
|
||||||
{dialogContent}
|
key="error"
|
||||||
</CSSTransition>
|
title={this.getTitle(error)}
|
||||||
)}
|
dismissable={false}
|
||||||
</TransitionGroup>
|
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 React from 'react'
|
||||||
import * as crypto from 'crypto'
|
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||||
import {
|
import {
|
||||||
IAppState,
|
IAppState,
|
||||||
|
@ -14,6 +13,7 @@ import { assertNever } from '../lib/fatal-error'
|
||||||
import { shell } from '../lib/app-shell'
|
import { shell } from '../lib/app-shell'
|
||||||
import { updateStore, UpdateStatus } from './lib/update-store'
|
import { updateStore, UpdateStatus } from './lib/update-store'
|
||||||
import { RetryAction } from '../models/retry-actions'
|
import { RetryAction } from '../models/retry-actions'
|
||||||
|
import { FetchType } from '../models/fetch'
|
||||||
import { shouldRenderApplicationMenu } from './lib/features'
|
import { shouldRenderApplicationMenu } from './lib/features'
|
||||||
import { matchExistingRepository } from '../lib/repository-matching'
|
import { matchExistingRepository } from '../lib/repository-matching'
|
||||||
import { getDotComAPIEndpoint } from '../lib/api'
|
import { getDotComAPIEndpoint } from '../lib/api'
|
||||||
|
@ -93,7 +93,10 @@ import { RepositoryStateCache } from '../lib/stores/repository-state-cache'
|
||||||
import { PopupType, Popup } from '../models/popup'
|
import { PopupType, Popup } from '../models/popup'
|
||||||
import { OversizedFiles } from './changes/oversized-files-warning'
|
import { OversizedFiles } from './changes/oversized-files-warning'
|
||||||
import { PushNeedsPullWarning } from './push-needs-pull'
|
import { PushNeedsPullWarning } from './push-needs-pull'
|
||||||
import { isCurrentBranchForcePush } from '../lib/rebase'
|
import {
|
||||||
|
ForcePushBranchState,
|
||||||
|
getCurrentBranchForcePushState,
|
||||||
|
} from '../lib/rebase'
|
||||||
import { Banner, BannerType } from '../models/banner'
|
import { Banner, BannerType } from '../models/banner'
|
||||||
import { StashAndSwitchBranch } from './stash-changes/stash-and-switch-branch-dialog'
|
import { StashAndSwitchBranch } from './stash-changes/stash-and-switch-branch-dialog'
|
||||||
import { OverwriteStash } from './stash-changes/overwrite-stashed-changes-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 { showContextualMenu } from '../lib/menu-item'
|
||||||
import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog'
|
import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog'
|
||||||
import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-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 MinuteInMilliseconds = 1000 * 60
|
||||||
const HourInMilliseconds = MinuteInMilliseconds * 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.
|
* modal dialog such as the preferences, or an error dialog.
|
||||||
*/
|
*/
|
||||||
private get isShowingModal() {
|
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
|
* passed popupType, so it can be used in render() without creating
|
||||||
* multiple instances when the component gets re-rendered.
|
* multiple instances when the component gets re-rendered.
|
||||||
*/
|
*/
|
||||||
private getOnPopupDismissedFn = memoizeOne((popupType: PopupType) => {
|
private getOnPopupDismissedFn = memoizeOne((popupId: string) => {
|
||||||
return () => this.onPopupDismissed(popupType)
|
return () => this.onPopupDismissed(popupId)
|
||||||
})
|
})
|
||||||
|
|
||||||
public constructor(props: IAppProps) {
|
public constructor(props: IAppProps) {
|
||||||
|
@ -278,7 +287,14 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
updateStore.onError(error => {
|
updateStore.onError(error => {
|
||||||
log.error(`Error checking for updates`, 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) => {
|
ipcRenderer.on('launch-timing-stats', (_, stats) => {
|
||||||
|
@ -346,7 +362,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
|
|
||||||
private onMenuEvent(name: MenuEvent): any {
|
private onMenuEvent(name: MenuEvent): any {
|
||||||
// Don't react to menu events when an error dialog is shown.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,6 +373,8 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
return this.push({ forceWithLease: true })
|
return this.push({ forceWithLease: true })
|
||||||
case 'pull':
|
case 'pull':
|
||||||
return this.pull()
|
return this.pull()
|
||||||
|
case 'fetch':
|
||||||
|
return this.fetch()
|
||||||
case 'show-changes':
|
case 'show-changes':
|
||||||
return this.showChanges()
|
return this.showChanges()
|
||||||
case 'show-history':
|
case 'show-history':
|
||||||
|
@ -421,7 +439,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
return this.goToCommitMessage()
|
return this.goToCommitMessage()
|
||||||
case 'open-pull-request':
|
case 'open-pull-request':
|
||||||
return this.openPullRequest()
|
return this.openPullRequest()
|
||||||
case 'start-pull-request':
|
case 'preview-pull-request':
|
||||||
return this.startPullRequest()
|
return this.startPullRequest()
|
||||||
case 'install-cli':
|
case 'install-cli':
|
||||||
return this.props.dispatcher.installCLI()
|
return this.props.dispatcher.installCLI()
|
||||||
|
@ -443,6 +461,10 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
return this.findText()
|
return this.findText()
|
||||||
case 'pull-request-check-run-failed':
|
case 'pull-request-check-run-failed':
|
||||||
return this.testPullRequestCheckRunFailed()
|
return this.testPullRequestCheckRunFailed()
|
||||||
|
case 'show-app-error':
|
||||||
|
return this.props.dispatcher.postError(
|
||||||
|
new Error('Test Error - to use default error handler' + uuid())
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return assertNever(name, `Unknown menu event name: ${name}`)
|
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)
|
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() {
|
private showStashedChanges() {
|
||||||
const state = this.state.selectedState
|
const state = this.state.selectedState
|
||||||
if (state == null || state.type !== SelectionType.Repository) {
|
if (state == null || state.type !== SelectionType.Repository) {
|
||||||
|
@ -1352,8 +1383,8 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPopupDismissed = (popupType: PopupType) => {
|
private onPopupDismissed = (popupId: string) => {
|
||||||
return this.props.dispatcher.closePopup(popupType)
|
return this.props.dispatcher.closePopupById(popupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onContinueWithUntrustedCertificate = (
|
private onContinueWithUntrustedCertificate = (
|
||||||
|
@ -1368,19 +1399,44 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
private onUpdateAvailableDismissed = () =>
|
private onUpdateAvailableDismissed = () =>
|
||||||
this.props.dispatcher.setUpdateBannerVisibility(false)
|
this.props.dispatcher.setUpdateBannerVisibility(false)
|
||||||
|
|
||||||
private currentPopupContent(): JSX.Element | null {
|
private allPopupContent(): JSX.Element | null {
|
||||||
// Hide any dialogs while we're displaying an error
|
let { allPopups } = this.state
|
||||||
if (this.state.errors.length) {
|
|
||||||
|
if (!enableStackedPopups() && this.state.currentPopup !== null) {
|
||||||
|
allPopups = [this.state.currentPopup]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allPopups.length === 0) {
|
||||||
return null
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPopupDismissedFn = this.getOnPopupDismissedFn(popup.type)
|
const onPopupDismissedFn = this.getOnPopupDismissedFn(popup.id)
|
||||||
|
|
||||||
switch (popup.type) {
|
switch (popup.type) {
|
||||||
case PopupType.RenameBranch:
|
case PopupType.RenameBranch:
|
||||||
|
@ -1481,6 +1537,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
confirmDiscardChangesPermanently={
|
confirmDiscardChangesPermanently={
|
||||||
this.state.askForConfirmationOnDiscardChangesPermanently
|
this.state.askForConfirmationOnDiscardChangesPermanently
|
||||||
}
|
}
|
||||||
|
confirmDiscardStash={this.state.askForConfirmationOnDiscardStash}
|
||||||
confirmForcePush={this.state.askForConfirmationOnForcePush}
|
confirmForcePush={this.state.askForConfirmationOnForcePush}
|
||||||
confirmUndoCommit={this.state.askForConfirmationOnUndoCommit}
|
confirmUndoCommit={this.state.askForConfirmationOnUndoCommit}
|
||||||
uncommittedChangesStrategy={this.state.uncommittedChangesStrategy}
|
uncommittedChangesStrategy={this.state.uncommittedChangesStrategy}
|
||||||
|
@ -1542,6 +1599,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
onDismissed={onPopupDismissedFn}
|
onDismissed={onPopupDismissedFn}
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
initialPath={popup.path}
|
initialPath={popup.path}
|
||||||
|
isTopMost={isTopMost}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case PopupType.CloneRepository:
|
case PopupType.CloneRepository:
|
||||||
|
@ -1557,6 +1615,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
onTabSelected={this.onCloneRepositoriesTabSelected}
|
onTabSelected={this.onCloneRepositoriesTabSelected}
|
||||||
apiRepositories={this.state.apiRepositories}
|
apiRepositories={this.state.apiRepositories}
|
||||||
onRefreshRepositories={this.onRefreshRepositories}
|
onRefreshRepositories={this.onRefreshRepositories}
|
||||||
|
isTopMost={isTopMost}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case PopupType.CreateBranch: {
|
case PopupType.CreateBranch: {
|
||||||
|
@ -1617,6 +1676,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
onCheckForNonStaggeredUpdates={this.onCheckForNonStaggeredUpdates}
|
onCheckForNonStaggeredUpdates={this.onCheckForNonStaggeredUpdates}
|
||||||
onShowAcknowledgements={this.showAcknowledgements}
|
onShowAcknowledgements={this.showAcknowledgements}
|
||||||
onShowTermsAndConditions={this.showTermsAndConditions}
|
onShowTermsAndConditions={this.showTermsAndConditions}
|
||||||
|
isTopMost={isTopMost}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case PopupType.PublishRepository:
|
case PopupType.PublishRepository:
|
||||||
|
@ -1849,6 +1909,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
<ConfirmDiscardStashDialog
|
<ConfirmDiscardStashDialog
|
||||||
key="confirm-discard-stash-dialog"
|
key="confirm-discard-stash-dialog"
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
|
askForConfirmationOnDiscardStash={
|
||||||
|
this.state.askForConfirmationOnDiscardStash
|
||||||
|
}
|
||||||
repository={repository}
|
repository={repository}
|
||||||
stash={stash}
|
stash={stash}
|
||||||
onDismissed={onPopupDismissedFn}
|
onDismissed={onPopupDismissedFn}
|
||||||
|
@ -2243,36 +2306,73 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case PopupType.StartPullRequest: {
|
case PopupType.StartPullRequest: {
|
||||||
const { selectedState } = this.state
|
// Intentionally chose to get the current pull request state on
|
||||||
if (
|
// rerender because state variables such as file selection change
|
||||||
selectedState == null ||
|
// via the dispatcher.
|
||||||
selectedState.type !== SelectionType.Repository
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { state: repoState, repository } = selectedState
|
const { pullRequestFilesListWidth, hideWhitespaceInPullRequestDiff } =
|
||||||
const { pullRequestState, branchesState } = repoState
|
this.state
|
||||||
if (
|
|
||||||
pullRequestState === null ||
|
const {
|
||||||
branchesState.tip.kind !== TipState.Valid
|
prBaseBranches,
|
||||||
) {
|
currentBranch,
|
||||||
return null
|
defaultBranch,
|
||||||
}
|
imageDiffType,
|
||||||
const { allBranches, recentBranches, defaultBranch, tip } =
|
externalEditorLabel,
|
||||||
branchesState
|
nonLocalCommitSHA,
|
||||||
const currentBranch = tip.branch
|
prRecentBaseBranches,
|
||||||
|
repository,
|
||||||
|
showSideBySideDiff,
|
||||||
|
currentBranchHasPullRequest,
|
||||||
|
} = popup
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenPullRequestDialog
|
<OpenPullRequestDialog
|
||||||
key="open-pull-request"
|
key="open-pull-request"
|
||||||
allBranches={allBranches}
|
prBaseBranches={prBaseBranches}
|
||||||
currentBranch={currentBranch}
|
currentBranch={currentBranch}
|
||||||
defaultBranch={defaultBranch}
|
defaultBranch={defaultBranch}
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
|
fileListWidth={pullRequestFilesListWidth}
|
||||||
|
hideWhitespaceInDiff={hideWhitespaceInPullRequestDiff}
|
||||||
|
imageDiffType={imageDiffType}
|
||||||
|
nonLocalCommitSHA={nonLocalCommitSHA}
|
||||||
pullRequestState={pullRequestState}
|
pullRequestState={pullRequestState}
|
||||||
recentBranches={recentBranches}
|
prRecentBaseBranches={prRecentBaseBranches}
|
||||||
repository={repository}
|
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}
|
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(
|
private getWarnForcePushDialogOnBegin(
|
||||||
onBegin: () => void,
|
onBegin: () => void,
|
||||||
onPopupDismissedFn: () => void
|
onPopupDismissedFn: () => void
|
||||||
|
@ -2375,8 +2487,8 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
this.props.dispatcher.showPopup({ type: PopupType.TermsAndConditions })
|
this.props.dispatcher.showPopup({ type: PopupType.TermsAndConditions })
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPopup() {
|
private renderPopups() {
|
||||||
const popupContent = this.currentPopupContent()
|
const popupContent = this.allPopupContent()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransitionGroup>
|
<TransitionGroup>
|
||||||
|
@ -2430,8 +2542,6 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
return <FullScreenInfo windowState={this.state.windowState} />
|
return <FullScreenInfo windowState={this.state.windowState} />
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearError = (error: Error) => this.props.dispatcher.clearError(error)
|
|
||||||
|
|
||||||
private onConfirmDiscardChangesChanged = (value: boolean) => {
|
private onConfirmDiscardChangesChanged = (value: boolean) => {
|
||||||
this.props.dispatcher.setConfirmDiscardChangesSetting(value)
|
this.props.dispatcher.setConfirmDiscardChangesSetting(value)
|
||||||
}
|
}
|
||||||
|
@ -2440,17 +2550,6 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
this.props.dispatcher.setConfirmDiscardChangesPermanentlySetting(value)
|
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) => {
|
private onRetryAction = (retryAction: RetryAction) => {
|
||||||
this.props.dispatcher.performRetry(retryAction)
|
this.props.dispatcher.performRetry(retryAction)
|
||||||
}
|
}
|
||||||
|
@ -2477,8 +2576,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
{this.renderToolbar()}
|
{this.renderToolbar()}
|
||||||
{this.renderBanner()}
|
{this.renderBanner()}
|
||||||
{this.renderRepository()}
|
{this.renderRepository()}
|
||||||
{this.renderPopup()}
|
{this.renderPopups()}
|
||||||
{this.renderAppError()}
|
|
||||||
{this.renderDragElement()}
|
{this.renderDragElement()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -2702,7 +2800,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
remoteName = tip.branch.upstreamRemoteName
|
remoteName = tip.branch.upstreamRemoteName
|
||||||
}
|
}
|
||||||
|
|
||||||
const isForcePush = isCurrentBranchForcePush(branchesState, aheadBehind)
|
const isForcePush =
|
||||||
|
getCurrentBranchForcePushState(branchesState, aheadBehind) ===
|
||||||
|
ForcePushBranchState.Recommended
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PushPullButton
|
<PushPullButton
|
||||||
|
@ -2955,6 +3055,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
askForConfirmationOnDiscardChanges={
|
askForConfirmationOnDiscardChanges={
|
||||||
state.askForConfirmationOnDiscardChanges
|
state.askForConfirmationOnDiscardChanges
|
||||||
}
|
}
|
||||||
|
askForConfirmationOnDiscardStash={
|
||||||
|
state.askForConfirmationOnDiscardStash
|
||||||
|
}
|
||||||
accounts={state.accounts}
|
accounts={state.accounts}
|
||||||
externalEditorLabel={externalEditorLabel}
|
externalEditorLabel={externalEditorLabel}
|
||||||
resolvedExternalEditor={state.resolvedExternalEditor}
|
resolvedExternalEditor={state.resolvedExternalEditor}
|
||||||
|
@ -2967,6 +3070,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
aheadBehindStore={this.props.aheadBehindStore}
|
aheadBehindStore={this.props.aheadBehindStore}
|
||||||
commitSpellcheckEnabled={this.state.commitSpellcheckEnabled}
|
commitSpellcheckEnabled={this.state.commitSpellcheckEnabled}
|
||||||
onCherryPick={this.startCherryPickWithoutBranch}
|
onCherryPick={this.startCherryPickWithoutBranch}
|
||||||
|
pullRequestSuggestedNextAction={state.pullRequestSuggestedNextAction}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (selectedState.type === SelectionType.CloningRepository) {
|
} else if (selectedState.type === SelectionType.CloningRepository) {
|
||||||
|
@ -3049,22 +3153,17 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = repository.gitHubRepository.htmlURL
|
const commitURL = createCommitURL(
|
||||||
|
repository.gitHubRepository,
|
||||||
|
SHA,
|
||||||
|
filePath
|
||||||
|
)
|
||||||
|
|
||||||
let fileSuffix = ''
|
if (commitURL === null) {
|
||||||
if (filePath != null) {
|
return
|
||||||
const fileHash = crypto
|
|
||||||
.createHash('sha256')
|
|
||||||
.update(filePath)
|
|
||||||
.digest('hex')
|
|
||||||
fileSuffix = '#diff-' + fileHash
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseURL) {
|
this.props.dispatcher.openInBrowser(commitURL)
|
||||||
this.props.dispatcher.openInBrowser(
|
|
||||||
`${baseURL}/commit/${SHA}${fileSuffix}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onBranchDeleted = (repository: Repository) => {
|
private onBranchDeleted = (repository: Repository) => {
|
||||||
|
|
|
@ -110,6 +110,9 @@ interface IBranchListProps {
|
||||||
|
|
||||||
/** Called to render content before/above the branches filter and list. */
|
/** Called to render content before/above the branches filter and list. */
|
||||||
readonly renderPreList?: () => JSX.Element | null
|
readonly renderPreList?: () => JSX.Element | null
|
||||||
|
|
||||||
|
/** Optional: No branches message */
|
||||||
|
readonly noBranchesMessage?: string | JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IBranchListState {
|
interface IBranchListState {
|
||||||
|
@ -249,6 +252,7 @@ export class BranchList extends React.Component<
|
||||||
<NoBranches
|
<NoBranches
|
||||||
onCreateNewBranch={this.onCreateNewBranch}
|
onCreateNewBranch={this.onCreateNewBranch}
|
||||||
canCreateNewBranch={this.props.canCreateNewBranch}
|
canCreateNewBranch={this.props.canCreateNewBranch}
|
||||||
|
noBranchesMessage={this.props.noBranchesMessage}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { IMatches } from '../../lib/fuzzy-find'
|
import { IMatches } from '../../lib/fuzzy-find'
|
||||||
import { Branch } from '../../models/branch'
|
import { Branch } from '../../models/branch'
|
||||||
import { Button } from '../lib/button'
|
|
||||||
import { ClickSource } from '../lib/list'
|
import { ClickSource } from '../lib/list'
|
||||||
import { Popover } from '../lib/popover'
|
import { PopoverDropdown } from '../lib/popover-dropdown'
|
||||||
import { Ref } from '../lib/ref'
|
|
||||||
import { Octicon } from '../octicons'
|
|
||||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
|
||||||
import { BranchList } from './branch-list'
|
import { BranchList } from './branch-list'
|
||||||
import { renderDefaultBranch } from './branch-renderer'
|
import { renderDefaultBranch } from './branch-renderer'
|
||||||
import { IBranchListItem } from './group-branches'
|
import { IBranchListItem } from './group-branches'
|
||||||
|
|
||||||
const defaultDropdownListHeight = 300
|
|
||||||
const maxDropdownListHeight = 500
|
|
||||||
|
|
||||||
interface IBranchSelectProps {
|
interface IBranchSelectProps {
|
||||||
/** The initially selected branch. */
|
/** The initially selected branch. */
|
||||||
readonly branch: Branch
|
readonly branch: Branch | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See IBranchesState.defaultBranch
|
* See IBranchesState.defaultBranch
|
||||||
|
@ -40,13 +33,14 @@ interface IBranchSelectProps {
|
||||||
|
|
||||||
/** Called when the user changes the selected branch. */
|
/** Called when the user changes the selected branch. */
|
||||||
readonly onChange?: (branch: Branch) => void
|
readonly onChange?: (branch: Branch) => void
|
||||||
|
|
||||||
|
/** Optional: No branches message */
|
||||||
|
readonly noBranchesMessage?: string | JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IBranchSelectState {
|
interface IBranchSelectState {
|
||||||
readonly showBranchDropdown: boolean
|
|
||||||
readonly selectedBranch: Branch | null
|
readonly selectedBranch: Branch | null
|
||||||
readonly filterText: string
|
readonly filterText: string
|
||||||
readonly dropdownListHeight: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,67 +50,25 @@ export class BranchSelect extends React.Component<
|
||||||
IBranchSelectProps,
|
IBranchSelectProps,
|
||||||
IBranchSelectState
|
IBranchSelectState
|
||||||
> {
|
> {
|
||||||
private invokeButtonRef: HTMLButtonElement | null = null
|
private popoverRef = React.createRef<PopoverDropdown>()
|
||||||
|
|
||||||
public constructor(props: IBranchSelectProps) {
|
public constructor(props: IBranchSelectProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showBranchDropdown: false,
|
|
||||||
selectedBranch: props.branch,
|
selectedBranch: props.branch,
|
||||||
filterText: '',
|
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) => {
|
private renderBranch = (item: IBranchListItem, matches: IMatches) => {
|
||||||
return renderDefaultBranch(item, matches, this.props.currentBranch)
|
return renderDefaultBranch(item, matches, this.props.currentBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onItemClick = (branch: Branch, source: ClickSource) => {
|
private onItemClick = (branch: Branch, source: ClickSource) => {
|
||||||
source.event.preventDefault()
|
source.event.preventDefault()
|
||||||
this.setState({ showBranchDropdown: false, selectedBranch: branch })
|
this.popoverRef.current?.closePopover()
|
||||||
|
this.setState({ selectedBranch: branch })
|
||||||
this.props.onChange?.(branch)
|
this.props.onChange?.(branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,67 +76,38 @@ export class BranchSelect extends React.Component<
|
||||||
this.setState({ filterText })
|
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() {
|
public render() {
|
||||||
|
const {
|
||||||
|
currentBranch,
|
||||||
|
defaultBranch,
|
||||||
|
recentBranches,
|
||||||
|
allBranches,
|
||||||
|
noBranchesMessage,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const { filterText, selectedBranch } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="branch-select-component">
|
<PopoverDropdown
|
||||||
<Button
|
contentTitle="Choose a base branch"
|
||||||
onClick={this.toggleBranchDropdown}
|
buttonContent={selectedBranch?.name ?? ''}
|
||||||
onButtonRef={this.onInvokeButtonRef}
|
label="base:"
|
||||||
>
|
ref={this.popoverRef}
|
||||||
<Ref>
|
>
|
||||||
<span className="base-label">base:</span>
|
<BranchList
|
||||||
{this.state.selectedBranch?.name}
|
allBranches={allBranches}
|
||||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
currentBranch={currentBranch}
|
||||||
</Ref>
|
defaultBranch={defaultBranch}
|
||||||
</Button>
|
recentBranches={recentBranches}
|
||||||
{this.renderBranchDropdown()}
|
filterText={filterText}
|
||||||
</div>
|
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
|
readonly onCreateNewBranch: () => void
|
||||||
/** True to display the UI elements for creating a new branch, false to hide them */
|
/** True to display the UI elements for creating a new branch, false to hide them */
|
||||||
readonly canCreateNewBranch: boolean
|
readonly canCreateNewBranch: boolean
|
||||||
|
/** Optional: No branches message */
|
||||||
|
readonly noBranchesMessage?: string | JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NoBranches extends React.Component<INoBranchesProps> {
|
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() {
|
private renderShortcut() {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { Octicon, iconForStatus } from '../octicons'
|
||||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||||
import { mapStatus } from '../../lib/status'
|
import { mapStatus } from '../../lib/status'
|
||||||
import { DiffOptions } from '../diff/diff-options'
|
import { DiffOptions } from '../diff/diff-options'
|
||||||
import { RepositorySectionTab } from '../../lib/app-state'
|
|
||||||
|
|
||||||
interface IChangedFileDetailsProps {
|
interface IChangedFileDetailsProps {
|
||||||
readonly path: string
|
readonly path: string
|
||||||
|
@ -61,7 +60,7 @@ export class ChangedFileDetails extends React.Component<
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DiffOptions
|
<DiffOptions
|
||||||
sourceTab={RepositorySectionTab.Changes}
|
isInteractiveDiff={true}
|
||||||
onHideWhitespaceChangesChanged={
|
onHideWhitespaceChangesChanged={
|
||||||
this.props.onHideWhitespaceInDiffChanged
|
this.props.onHideWhitespaceInDiffChanged
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,21 @@ import { TipState, IValidBranch } from '../../models/tip'
|
||||||
import { Ref } from '../lib/ref'
|
import { Ref } from '../lib/ref'
|
||||||
import { IAheadBehind } from '../../models/branch'
|
import { IAheadBehind } from '../../models/branch'
|
||||||
import { IRemote } from '../../models/remote'
|
import { IRemote } from '../../models/remote'
|
||||||
import { isCurrentBranchForcePush } from '../../lib/rebase'
|
import {
|
||||||
|
ForcePushBranchState,
|
||||||
|
getCurrentBranchForcePushState,
|
||||||
|
} from '../../lib/rebase'
|
||||||
import { StashedChangesLoadStates } from '../../models/stash-entry'
|
import { StashedChangesLoadStates } from '../../models/stash-entry'
|
||||||
import { Dispatcher } from '../dispatcher'
|
import { Dispatcher } from '../dispatcher'
|
||||||
import { SuggestedActionGroup } from '../suggested-actions'
|
import { SuggestedActionGroup } from '../suggested-actions'
|
||||||
import { PreferencesTab } from '../../models/preferences'
|
import { PreferencesTab } from '../../models/preferences'
|
||||||
import { PopupType } from '../../models/popup'
|
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) {
|
function formatMenuItemLabel(text: string) {
|
||||||
if (__WIN32__ || __LINUX__) {
|
if (__WIN32__ || __LINUX__) {
|
||||||
|
@ -68,6 +77,9 @@ interface INoChangesProps {
|
||||||
* opening the repository in an external editor.
|
* opening the repository in an external editor.
|
||||||
*/
|
*/
|
||||||
readonly isExternalEditorAvailable: boolean
|
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)
|
return this.renderPublishBranchAction(tip)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isForcePush = isCurrentBranchForcePush(branchesState, aheadBehind)
|
const isForcePush =
|
||||||
|
getCurrentBranchForcePushState(branchesState, aheadBehind) ===
|
||||||
|
ForcePushBranchState.Recommended
|
||||||
if (isForcePush) {
|
if (isForcePush) {
|
||||||
// do not render an action currently after the rebase has completed, as
|
// do not render an action currently after the rebase has completed, as
|
||||||
// the default behaviour is currently to pull in changes from the tracking
|
// 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) {
|
private onPullRequestSuggestedActionChanged = (
|
||||||
const itemId: MenuIDs = 'create-pull-request'
|
action: PullRequestSuggestedNextAction
|
||||||
const menuItem = this.getMenuItemInfo(itemId)
|
) => {
|
||||||
|
this.props.dispatcher.setPullRequestSuggestedNextAction(action)
|
||||||
|
}
|
||||||
|
|
||||||
if (menuItem === undefined) {
|
private renderCreatePullRequestAction(tip: IValidBranch) {
|
||||||
log.error(`Could not find matching menu item for ${itemId}`)
|
const createMenuItem = this.getMenuItemInfo('create-pull-request')
|
||||||
|
if (createMenuItem === undefined) {
|
||||||
|
log.error(`Could not find matching menu item for 'create-pull-request'`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -652,17 +670,69 @@ export class NoChanges extends React.Component<
|
||||||
const title = `Create a Pull Request from your current branch`
|
const title = `Create a Pull Request from your current branch`
|
||||||
const buttonText = `Create Pull Request`
|
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 (
|
return (
|
||||||
<MenuBackedSuggestedAction
|
<DropdownSuggestedAction
|
||||||
key="create-pr-action"
|
key="pull-request-action"
|
||||||
title={title}
|
className="pull-request-action"
|
||||||
menuItemId={itemId}
|
suggestedActions={[previewPullRequestAction, createPullRequestAction]}
|
||||||
description={description}
|
selectedActionValue={this.props.pullRequestSuggestedNextAction}
|
||||||
buttonText={buttonText}
|
onSuggestedActionChanged={this.onPullRequestSuggestedActionChanged}
|
||||||
discoverabilityContent={this.renderDiscoverabilityElements(menuItem)}
|
|
||||||
type="primary"
|
|
||||||
disabled={!menuItem.enabled}
|
|
||||||
onClick={this.onCreatePullRequestClicked}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react'
|
||||||
import { Dispatcher } from '../dispatcher'
|
import { Dispatcher } from '../dispatcher'
|
||||||
import { getDefaultDir, setDefaultDir } from '../lib/default-dir'
|
import { getDefaultDir, setDefaultDir } from '../lib/default-dir'
|
||||||
import { Account } from '../../models/account'
|
import { Account } from '../../models/account'
|
||||||
|
import { FoldoutType } from '../../lib/app-state'
|
||||||
import {
|
import {
|
||||||
IRepositoryIdentifier,
|
IRepositoryIdentifier,
|
||||||
parseRepositoryIdentifier,
|
parseRepositoryIdentifier,
|
||||||
|
@ -23,6 +24,7 @@ import { ClickSource } from '../lib/list'
|
||||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||||
import { showOpenDialog, showSaveDialog } from '../main-process-proxy'
|
import { showOpenDialog, showSaveDialog } from '../main-process-proxy'
|
||||||
import { readdir } from 'fs/promises'
|
import { readdir } from 'fs/promises'
|
||||||
|
import { isTopMostDialog } from '../dialog/is-top-most'
|
||||||
|
|
||||||
interface ICloneRepositoryProps {
|
interface ICloneRepositoryProps {
|
||||||
readonly dispatcher: Dispatcher
|
readonly dispatcher: Dispatcher
|
||||||
|
@ -64,6 +66,9 @@ interface ICloneRepositoryProps {
|
||||||
* available for cloning.
|
* available for cloning.
|
||||||
*/
|
*/
|
||||||
readonly onRefreshRepositories: (account: Account) => void
|
readonly onRefreshRepositories: (account: Account) => void
|
||||||
|
|
||||||
|
/** Whether the dialog is the top most in the dialog stack */
|
||||||
|
readonly isTopMost: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICloneRepositoryState {
|
interface ICloneRepositoryState {
|
||||||
|
@ -147,6 +152,16 @@ export class CloneRepository extends React.Component<
|
||||||
ICloneRepositoryProps,
|
ICloneRepositoryProps,
|
||||||
ICloneRepositoryState
|
ICloneRepositoryState
|
||||||
> {
|
> {
|
||||||
|
private checkIsTopMostDialog = isTopMostDialog(
|
||||||
|
() => {
|
||||||
|
this.validatePath()
|
||||||
|
window.addEventListener('focus', this.onWindowFocus)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
window.removeEventListener('focus', this.onWindowFocus)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
public constructor(props: ICloneRepositoryProps) {
|
public constructor(props: ICloneRepositoryProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
@ -191,6 +206,8 @@ export class CloneRepository extends React.Component<
|
||||||
if (prevProps.initialURL !== this.props.initialURL) {
|
if (prevProps.initialURL !== this.props.initialURL) {
|
||||||
this.updateUrl(this.props.initialURL || '')
|
this.updateUrl(this.props.initialURL || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
@ -199,7 +216,11 @@ export class CloneRepository extends React.Component<
|
||||||
this.updateUrl(initialURL)
|
this.updateUrl(initialURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('focus', this.onWindowFocus)
|
this.checkIsTopMostDialog(this.props.isTopMost)
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
this.checkIsTopMostDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializePath = async () => {
|
private initializePath = async () => {
|
||||||
|
@ -223,10 +244,6 @@ export class CloneRepository extends React.Component<
|
||||||
this.updateUrl(selectedTabState.url)
|
this.updateUrl(selectedTabState.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
window.removeEventListener('focus', this.onWindowFocus)
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { error } = this.getSelectedTabState()
|
const { error } = this.getSelectedTabState()
|
||||||
return (
|
return (
|
||||||
|
@ -728,6 +745,7 @@ export class CloneRepository extends React.Component<
|
||||||
|
|
||||||
const { url, defaultBranch } = cloneInfo
|
const { url, defaultBranch } = cloneInfo
|
||||||
|
|
||||||
|
this.props.dispatcher.closeFoldout(FoldoutType.Repository)
|
||||||
try {
|
try {
|
||||||
this.cloneImpl(url.trim(), path, defaultBranch)
|
this.cloneImpl(url.trim(), path, defaultBranch)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -3,6 +3,32 @@ import classNames from 'classnames'
|
||||||
import { DialogHeader } from './header'
|
import { DialogHeader } from './header'
|
||||||
import { createUniqueId, releaseUniqueId } from '../lib/id-pool'
|
import { createUniqueId, releaseUniqueId } from '../lib/id-pool'
|
||||||
import { getTitleBarHeight } from '../window/title-bar'
|
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
|
* The time (in milliseconds) from when the dialog is mounted
|
||||||
|
@ -138,6 +164,18 @@ interface IDialogState {
|
||||||
* out of the dialog without first dismissing it.
|
* out of the dialog without first dismissing it.
|
||||||
*/
|
*/
|
||||||
export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
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 dialogElement: HTMLDialogElement | null = null
|
||||||
private dismissGraceTimeoutId?: number
|
private dismissGraceTimeoutId?: number
|
||||||
|
|
||||||
|
@ -214,6 +252,13 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
||||||
|
|
||||||
private onDismissGraceTimer = () => {
|
private onDismissGraceTimer = () => {
|
||||||
this.setState({ isAppearing: false })
|
this.setState({ isAppearing: false })
|
||||||
|
|
||||||
|
this.dialogElement?.dispatchEvent(
|
||||||
|
new CustomEvent('dialog-appeared', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private isDismissable() {
|
private isDismissable() {
|
||||||
|
@ -242,11 +287,17 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
if (!this.dialogElement) {
|
this.checkIsTopMostDialog(this.context.isTopMost)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDialogIsTopMost() {
|
||||||
|
if (this.dialogElement == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dialogElement.showModal()
|
if (!this.dialogElement.open) {
|
||||||
|
this.dialogElement.showModal()
|
||||||
|
}
|
||||||
|
|
||||||
// Provide an event that components can subscribe to in order to perform
|
// Provide an event that components can subscribe to in order to perform
|
||||||
// tasks such as re-layout after the dialog is visible
|
// 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)
|
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
|
* Attempts to move keyboard focus to the first _suitable_ child of the
|
||||||
* dialog.
|
* dialog.
|
||||||
|
@ -418,23 +483,19 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this.clearDismissGraceTimeout()
|
|
||||||
|
|
||||||
if (this.state.titleId) {
|
if (this.state.titleId) {
|
||||||
releaseUniqueId(this.state.titleId)
|
releaseUniqueId(this.state.titleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener('focus', this.onWindowFocus)
|
this.checkIsTopMostDialog(false)
|
||||||
document.removeEventListener('mouseup', this.onDocumentMouseUp)
|
|
||||||
|
|
||||||
this.resizeObserver.disconnect()
|
|
||||||
window.removeEventListener('resize', this.scheduleResizeEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate() {
|
public componentDidUpdate(prevProps: IDialogProps) {
|
||||||
if (!this.props.title && this.state.titleId) {
|
if (!this.props.title && this.state.titleId) {
|
||||||
this.updateTitleId()
|
this.updateTitleId()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.checkIsTopMostDialog(this.context.isTopMost)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDialogCancel = (e: Event | React.SyntheticEvent) => {
|
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)
|
CodeMirrorHost.updateDoc(this.codeMirror, this.props.value)
|
||||||
this.resizeObserver.observe(this.codeMirror.getWrapperElement())
|
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) => {
|
private onSwapDoc = (cm: Editor, oldDoc: Doc) => {
|
||||||
|
@ -199,6 +207,7 @@ export class CodeMirrorHost extends React.Component<ICodeMirrorHostProps, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resizeObserver.disconnect()
|
this.resizeObserver.disconnect()
|
||||||
|
document.removeEventListener('dialog-show', this.onDialogAppeared)
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: ICodeMirrorHostProps) {
|
public componentDidUpdate(prevProps: ICodeMirrorHostProps) {
|
||||||
|
|
|
@ -4,14 +4,13 @@ import { Octicon } from '../octicons'
|
||||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||||
import { RadioButton } from '../lib/radio-button'
|
import { RadioButton } from '../lib/radio-button'
|
||||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||||
import { RepositorySectionTab } from '../../lib/app-state'
|
|
||||||
|
|
||||||
interface IDiffOptionsProps {
|
interface IDiffOptionsProps {
|
||||||
readonly sourceTab: RepositorySectionTab
|
readonly isInteractiveDiff: boolean
|
||||||
readonly hideWhitespaceChanges: boolean
|
readonly hideWhitespaceChanges: boolean
|
||||||
readonly onHideWhitespaceChangesChanged: (
|
readonly onHideWhitespaceChangesChanged: (
|
||||||
hideWhitespaceChanges: boolean
|
hideWhitespaceChanges: boolean
|
||||||
) => Promise<void>
|
) => void
|
||||||
|
|
||||||
readonly showSideBySideDiff: boolean
|
readonly showSideBySideDiff: boolean
|
||||||
readonly onShowSideBySideDiffChanged: (showSideBySideDiff: boolean) => void
|
readonly onShowSideBySideDiffChanged: (showSideBySideDiff: boolean) => void
|
||||||
|
@ -144,7 +143,7 @@ export class DiffOptions extends React.Component<
|
||||||
__DARWIN__ ? 'Hide Whitespace Changes' : 'Hide whitespace changes'
|
__DARWIN__ ? 'Hide Whitespace Changes' : 'Hide whitespace changes'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{this.props.sourceTab === RepositorySectionTab.Changes && (
|
{this.props.isInteractiveDiff && (
|
||||||
<p className="secondary-text">
|
<p className="secondary-text">
|
||||||
Interacting with individual lines or hunks will be disabled while
|
Interacting with individual lines or hunks will be disabled while
|
||||||
hiding whitespace.
|
hiding whitespace.
|
||||||
|
|
|
@ -363,6 +363,16 @@ export class SideBySideDiffRow extends React.Component<
|
||||||
throw new Error(`Unexpected expansion type ${expansionType}`)
|
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(
|
private renderHunkExpansionHandle(
|
||||||
hunkIndex: number,
|
hunkIndex: number,
|
||||||
expansionType: DiffHunkExpansionType
|
expansionType: DiffHunkExpansionType
|
||||||
|
@ -372,7 +382,7 @@ export class SideBySideDiffRow extends React.Component<
|
||||||
<div
|
<div
|
||||||
className="hunk-expansion-handle"
|
className="hunk-expansion-handle"
|
||||||
onContextMenu={this.props.onContextMenuExpandHunk}
|
onContextMenu={this.props.onContextMenuExpandHunk}
|
||||||
style={{ width: this.props.lineNumberWidth }}
|
style={{ width: this.lineGutterWidth }}
|
||||||
>
|
>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -389,7 +399,7 @@ export class SideBySideDiffRow extends React.Component<
|
||||||
<div
|
<div
|
||||||
className="hunk-expansion-handle selectable hoverable"
|
className="hunk-expansion-handle selectable hoverable"
|
||||||
onClick={elementInfo.handler}
|
onClick={elementInfo.handler}
|
||||||
style={{ width: this.props.lineNumberWidth }}
|
style={{ width: this.lineGutterWidth }}
|
||||||
onContextMenu={this.props.onContextMenuExpandHunk}
|
onContextMenu={this.props.onContextMenuExpandHunk}
|
||||||
>
|
>
|
||||||
<TooltippedContent
|
<TooltippedContent
|
||||||
|
@ -426,6 +436,12 @@ export class SideBySideDiffRow extends React.Component<
|
||||||
return null
|
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 (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
<div
|
<div
|
||||||
|
@ -434,6 +450,7 @@ export class SideBySideDiffRow extends React.Component<
|
||||||
onMouseLeave={this.onMouseLeaveHunk}
|
onMouseLeave={this.onMouseLeaveHunk}
|
||||||
onClick={this.onClickHunk}
|
onClick={this.onClickHunk}
|
||||||
onContextMenu={this.onContextMenuHunk}
|
onContextMenu={this.onContextMenuHunk}
|
||||||
|
style={style}
|
||||||
></div>
|
></div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -452,10 +469,7 @@ export class SideBySideDiffRow extends React.Component<
|
||||||
) {
|
) {
|
||||||
if (!this.props.isDiffSelectable || isSelected === undefined) {
|
if (!this.props.isDiffSelectable || isSelected === undefined) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="line-number" style={{ width: this.lineGutterWidth }}>
|
||||||
className="line-number"
|
|
||||||
style={{ width: this.props.lineNumberWidth }}
|
|
||||||
>
|
|
||||||
{lineNumbers.map((lineNumber, index) => (
|
{lineNumbers.map((lineNumber, index) => (
|
||||||
<span key={index}>{lineNumber}</span>
|
<span key={index}>{lineNumber}</span>
|
||||||
))}
|
))}
|
||||||
|
@ -470,7 +484,7 @@ export class SideBySideDiffRow extends React.Component<
|
||||||
'line-selected': isSelected,
|
'line-selected': isSelected,
|
||||||
hover: this.props.isHunkHovered,
|
hover: this.props.isHunkHovered,
|
||||||
})}
|
})}
|
||||||
style={{ width: this.props.lineNumberWidth }}
|
style={{ width: this.lineGutterWidth }}
|
||||||
onMouseDown={this.onMouseDownLineNumber}
|
onMouseDown={this.onMouseDownLineNumber}
|
||||||
onContextMenu={this.onContextMenuLineNumber}
|
onContextMenu={this.onContextMenuLineNumber}
|
||||||
>
|
>
|
||||||
|
@ -493,7 +507,7 @@ export class SideBySideDiffRow extends React.Component<
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
[column === DiffColumn.Before ? 'marginRight' : 'marginLeft']:
|
[column === DiffColumn.Before ? 'marginRight' : 'marginLeft']:
|
||||||
this.props.lineNumberWidth + 10,
|
this.lineGutterWidth + 10,
|
||||||
marginTop: -10,
|
marginTop: -10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -256,7 +256,7 @@ export class SideBySideDiff extends React.Component<
|
||||||
: [DiffLineType.Add, DiffLineType.Context]
|
: [DiffLineType.Add, DiffLineType.Context]
|
||||||
: [DiffLineType.Add, DiffLineType.Delete, DiffLineType.Context]
|
: [DiffLineType.Add, DiffLineType.Delete, DiffLineType.Context]
|
||||||
|
|
||||||
const contents = this.props.diff.hunks
|
const contents = this.state.diff.hunks
|
||||||
.flatMap(h =>
|
.flatMap(h =>
|
||||||
h.lines
|
h.lines
|
||||||
.filter(line => lineTypes.includes(line.type))
|
.filter(line => lineTypes.includes(line.type))
|
||||||
|
|
|
@ -68,7 +68,10 @@ import { FetchType } from '../../models/fetch'
|
||||||
import { GitHubRepository } from '../../models/github-repository'
|
import { GitHubRepository } from '../../models/github-repository'
|
||||||
import { ManualConflictResolution } from '../../models/manual-conflict-resolution'
|
import { ManualConflictResolution } from '../../models/manual-conflict-resolution'
|
||||||
import { Popup, PopupType } from '../../models/popup'
|
import { Popup, PopupType } from '../../models/popup'
|
||||||
import { PullRequest } from '../../models/pull-request'
|
import {
|
||||||
|
PullRequest,
|
||||||
|
PullRequestSuggestedNextAction,
|
||||||
|
} from '../../models/pull-request'
|
||||||
import {
|
import {
|
||||||
Repository,
|
Repository,
|
||||||
RepositoryWithGitHubRepository,
|
RepositoryWithGitHubRepository,
|
||||||
|
@ -384,6 +387,13 @@ export class Dispatcher {
|
||||||
return this.appStore._closePopup(popupType)
|
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. */
|
/** Show the foldout. This will close any current popup. */
|
||||||
public showFoldout(foldout: Foldout): Promise<void> {
|
public showFoldout(foldout: Foldout): Promise<void> {
|
||||||
return this.appStore._showFoldout(foldout)
|
return this.appStore._showFoldout(foldout)
|
||||||
|
@ -765,11 +775,6 @@ export class Dispatcher {
|
||||||
return this.appStore._pushError(error)
|
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
|
* Clone a missing repository to the previous path, and update it's
|
||||||
* state in the repository list if the clone completes without error.
|
* 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 */
|
/** Change the side by side diff setting */
|
||||||
public onShowSideBySideDiffChanged(showSideBySideDiff: boolean) {
|
public onShowSideBySideDiffChanged(showSideBySideDiff: boolean) {
|
||||||
return this.appStore._setShowSideBySideDiff(showSideBySideDiff)
|
return this.appStore._setShowSideBySideDiff(showSideBySideDiff)
|
||||||
|
@ -2175,8 +2193,11 @@ export class Dispatcher {
|
||||||
* openCreatePullRequestInBrowser method which immediately opens the
|
* openCreatePullRequestInBrowser method which immediately opens the
|
||||||
* create pull request page without showing a dialog.
|
* create pull request page without showing a dialog.
|
||||||
*/
|
*/
|
||||||
public createPullRequest(repository: Repository): Promise<void> {
|
public createPullRequest(
|
||||||
return this.appStore._createPullRequest(repository)
|
repository: Repository,
|
||||||
|
baseBranch?: Branch
|
||||||
|
): Promise<void> {
|
||||||
|
return this.appStore._createPullRequest(repository, baseBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2338,6 +2359,10 @@ export class Dispatcher {
|
||||||
await this.appStore._loadStatus(repository)
|
await this.appStore._loadStatus(repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setConfirmDiscardStashSetting(value: boolean) {
|
||||||
|
return this.appStore._setConfirmDiscardStashSetting(value)
|
||||||
|
}
|
||||||
|
|
||||||
public setConfirmForcePushSetting(value: boolean) {
|
public setConfirmForcePushSetting(value: boolean) {
|
||||||
return this.appStore._setConfirmForcePushSetting(value)
|
return this.appStore._setConfirmForcePushSetting(value)
|
||||||
}
|
}
|
||||||
|
@ -2435,6 +2460,10 @@ export class Dispatcher {
|
||||||
return this.statsStore.recordCreatePullRequest()
|
return this.statsStore.recordCreatePullRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public recordCreatePullRequestFromPreview() {
|
||||||
|
return this.statsStore.recordCreatePullRequestFromPreview()
|
||||||
|
}
|
||||||
|
|
||||||
public recordWelcomeWizardInitiated() {
|
public recordWelcomeWizardInitiated() {
|
||||||
return this.statsStore.recordWelcomeWizardInitiated()
|
return this.statsStore.recordWelcomeWizardInitiated()
|
||||||
}
|
}
|
||||||
|
@ -3963,9 +3992,63 @@ export class Dispatcher {
|
||||||
|
|
||||||
public startPullRequest(repository: Repository) {
|
public startPullRequest(repository: Repository) {
|
||||||
this.appStore._startPullRequest(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 { Octicon } from './octicons'
|
||||||
import * as OcticonSymbol from './octicons/octicons.generated'
|
import * as OcticonSymbol from './octicons/octicons.generated'
|
||||||
|
|
||||||
export interface IDropdownSelectButtonOption {
|
export interface IDropdownSelectButtonOption<T extends string> {
|
||||||
/** The select option header label. */
|
/** The select option header label. */
|
||||||
readonly label?: string | JSX.Element
|
readonly label?: string | JSX.Element
|
||||||
|
|
||||||
|
@ -12,15 +12,15 @@ export interface IDropdownSelectButtonOption {
|
||||||
readonly description?: string | JSX.Element
|
readonly description?: string | JSX.Element
|
||||||
|
|
||||||
/** The select option's value */
|
/** The select option's value */
|
||||||
readonly value?: string
|
readonly value: T
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDropdownSelectButtonProps {
|
interface IDropdownSelectButtonProps<T extends string> {
|
||||||
/** The selection button options */
|
/** The selection button options */
|
||||||
readonly options: ReadonlyArray<IDropdownSelectButtonOption>
|
readonly options: ReadonlyArray<IDropdownSelectButtonOption<T>>
|
||||||
|
|
||||||
/** The selection option value */
|
/** The selection option value */
|
||||||
readonly selectedValue?: string
|
readonly selectedValue?: T
|
||||||
|
|
||||||
/** Whether or not the button is enabled */
|
/** Whether or not the button is enabled */
|
||||||
readonly disabled?: boolean
|
readonly disabled?: boolean
|
||||||
|
@ -30,22 +30,22 @@ interface IDropdownSelectButtonProps {
|
||||||
|
|
||||||
/** Callback for when the button selection changes*/
|
/** Callback for when the button selection changes*/
|
||||||
readonly onSelectChange?: (
|
readonly onSelectChange?: (
|
||||||
selectedOption: IDropdownSelectButtonOption
|
selectedOption: IDropdownSelectButtonOption<T>
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
/** Callback for when button is selected option button is clicked */
|
/** Callback for when button is selected option button is clicked */
|
||||||
readonly onSubmit?: (
|
readonly onSubmit?: (
|
||||||
event: React.MouseEvent<HTMLButtonElement>,
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
selectedOption: IDropdownSelectButtonOption
|
selectedOption: IDropdownSelectButtonOption<T>
|
||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDropdownSelectButtonState {
|
interface IDropdownSelectButtonState<T extends string> {
|
||||||
/** Whether the options are rendered */
|
/** Whether the options are rendered */
|
||||||
readonly showButtonOptions: boolean
|
readonly showButtonOptions: boolean
|
||||||
|
|
||||||
/** The currently selected option */
|
/** The currently selected option */
|
||||||
readonly selectedOption: IDropdownSelectButtonOption | null
|
readonly selectedOption: IDropdownSelectButtonOption<T> | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The adjusting position of the options popover. This is calculated based
|
* The adjusting position of the options popover. This is calculated based
|
||||||
|
@ -54,14 +54,16 @@ interface IDropdownSelectButtonState {
|
||||||
readonly optionsPositionBottom?: string
|
readonly optionsPositionBottom?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DropdownSelectButton extends React.Component<
|
export class DropdownSelectButton<
|
||||||
IDropdownSelectButtonProps,
|
T extends string = string
|
||||||
IDropdownSelectButtonState
|
> extends React.Component<
|
||||||
|
IDropdownSelectButtonProps<T>,
|
||||||
|
IDropdownSelectButtonState<T>
|
||||||
> {
|
> {
|
||||||
private invokeButtonRef: HTMLButtonElement | null = null
|
private invokeButtonRef: HTMLButtonElement | null = null
|
||||||
private optionsContainerRef: HTMLDivElement | null = null
|
private optionsContainerRef: HTMLDivElement | null = null
|
||||||
|
|
||||||
public constructor(props: IDropdownSelectButtonProps) {
|
public constructor(props: IDropdownSelectButtonProps<T>) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
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(
|
private getSelectedOption(
|
||||||
selectedValue: string | undefined
|
selectedValue: T | undefined
|
||||||
): IDropdownSelectButtonOption | null {
|
): IDropdownSelectButtonOption<T> | null {
|
||||||
const { options } = this.props
|
const { options } = this.props
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
return null
|
return null
|
||||||
|
@ -104,8 +161,10 @@ export class DropdownSelectButton extends React.Component<
|
||||||
return selectedOption
|
return selectedOption
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSelectionChange = (selectedOption: IDropdownSelectButtonOption) => {
|
private onSelectionChange = (
|
||||||
return (_event: React.MouseEvent<HTMLLIElement, MouseEvent>) => {
|
selectedOption: IDropdownSelectButtonOption<T>
|
||||||
|
) => {
|
||||||
|
return (_event?: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||||
this.setState({ selectedOption, showButtonOptions: false })
|
this.setState({ selectedOption, showButtonOptions: false })
|
||||||
|
|
||||||
const { onSelectChange } = this.props
|
const { onSelectChange } = this.props
|
||||||
|
@ -127,7 +186,7 @@ export class DropdownSelectButton extends React.Component<
|
||||||
this.optionsContainerRef = ref
|
this.optionsContainerRef = ref
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSelectedIcon(option: IDropdownSelectButtonOption) {
|
private renderSelectedIcon(option: IDropdownSelectButtonOption<T>) {
|
||||||
const { selectedOption } = this.state
|
const { selectedOption } = this.state
|
||||||
if (selectedOption === null || option.value !== selectedOption.value) {
|
if (selectedOption === null || option.value !== selectedOption.value) {
|
||||||
return
|
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() {
|
private renderSplitButtonOptions() {
|
||||||
if (!this.state.showButtonOptions) {
|
if (!this.state.showButtonOptions) {
|
||||||
return
|
return
|
||||||
|
@ -150,22 +219,14 @@ export class DropdownSelectButton extends React.Component<
|
||||||
const { optionsPositionBottom: bottom } = this.state
|
const { optionsPositionBottom: bottom } = this.state
|
||||||
const openClass = bottom !== undefined ? 'open-top' : 'open-bottom'
|
const openClass = bottom !== undefined ? 'open-top' : 'open-bottom'
|
||||||
const classes = classNames('dropdown-select-button-options', openClass)
|
const classes = classNames('dropdown-select-button-options', openClass)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classes}
|
className={classes}
|
||||||
style={{ bottom }}
|
style={{ bottom }}
|
||||||
ref={this.onOptionsContainerRef}
|
ref={this.onOptionsContainerRef}
|
||||||
>
|
>
|
||||||
<ul>
|
{options.map(o => this.renderOption(o))}
|
||||||
{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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -199,23 +260,25 @@ export class DropdownSelectButton extends React.Component<
|
||||||
// method.
|
// method.
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
<Button
|
<div className="dropdown-button-wrappers">
|
||||||
className="invoke-button"
|
<Button
|
||||||
disabled={disabled}
|
className="invoke-button"
|
||||||
type="submit"
|
disabled={disabled}
|
||||||
tooltip={this.props.tooltip}
|
type="submit"
|
||||||
onButtonRef={this.onInvokeButtonRef}
|
tooltip={this.props.tooltip}
|
||||||
onClick={this.onSubmit}
|
onButtonRef={this.onInvokeButtonRef}
|
||||||
>
|
onClick={this.onSubmit}
|
||||||
{selectedOption.label}
|
>
|
||||||
</Button>
|
{selectedOption.label}
|
||||||
<Button
|
</Button>
|
||||||
className={dropdownClasses}
|
<Button
|
||||||
onClick={this.openSplitButtonDropdown}
|
className={dropdownClasses}
|
||||||
type="button"
|
onClick={this.openSplitButtonDropdown}
|
||||||
>
|
type="button"
|
||||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
>
|
||||||
</Button>
|
<Octicon symbol={OcticonSymbol.triangleDown} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{this.renderSplitButtonOptions()}
|
{this.renderSplitButtonOptions()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -219,6 +219,10 @@ export class CommitListItem extends React.PureComponent<
|
||||||
clipboard.writeText(this.props.commit.sha)
|
clipboard.writeText(this.props.commit.sha)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onCopyTags = () => {
|
||||||
|
clipboard.writeText(this.props.commit.tags.join(' '))
|
||||||
|
}
|
||||||
|
|
||||||
private onViewOnGitHub = () => {
|
private onViewOnGitHub = () => {
|
||||||
if (this.props.onViewCommitOnGitHub) {
|
if (this.props.onViewCommitOnGitHub) {
|
||||||
this.props.onViewCommitOnGitHub(this.props.commit.sha)
|
this.props.onViewCommitOnGitHub(this.props.commit.sha)
|
||||||
|
@ -341,7 +345,10 @@ export class CommitListItem extends React.PureComponent<
|
||||||
deleteTagsMenuItem
|
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(
|
items.push(
|
||||||
{
|
{
|
||||||
label: __DARWIN__ ? 'Cherry-pick Commit…' : 'Cherry-pick commit…',
|
label: __DARWIN__ ? 'Cherry-pick Commit…' : 'Cherry-pick commit…',
|
||||||
|
@ -353,6 +360,11 @@ export class CommitListItem extends React.PureComponent<
|
||||||
label: 'Copy SHA',
|
label: 'Copy SHA',
|
||||||
action: this.onCopySHA,
|
action: this.onCopySHA,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: __DARWIN__ ? darwinTagsLabel : windowTagsLabel,
|
||||||
|
action: this.onCopyTags,
|
||||||
|
enabled: this.props.commit.tags.length > 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: viewOnGitHubLabel,
|
label: viewOnGitHubLabel,
|
||||||
action: this.onViewOnGitHub,
|
action: this.onViewOnGitHub,
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { CommitAttribution } from '../lib/commit-attribution'
|
||||||
import { Tokenizer, TokenResult } from '../../lib/text-token-parser'
|
import { Tokenizer, TokenResult } from '../../lib/text-token-parser'
|
||||||
import { wrapRichTextCommitMessage } from '../../lib/wrap-rich-text-commit-message'
|
import { wrapRichTextCommitMessage } from '../../lib/wrap-rich-text-commit-message'
|
||||||
import { DiffOptions } from '../diff/diff-options'
|
import { DiffOptions } from '../diff/diff-options'
|
||||||
import { RepositorySectionTab } from '../../lib/app-state'
|
|
||||||
import { IChangesetData } from '../../lib/git'
|
import { IChangesetData } from '../../lib/git'
|
||||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||||
import { AppFileStatusKind } from '../../models/status'
|
import { AppFileStatusKind } from '../../models/status'
|
||||||
|
@ -431,7 +430,10 @@ export class CommitSummary extends React.Component<
|
||||||
aria-label="SHA"
|
aria-label="SHA"
|
||||||
>
|
>
|
||||||
<Octicon symbol={OcticonSymbol.gitCommit} />
|
<Octicon symbol={OcticonSymbol.gitCommit} />
|
||||||
<TooltippedCommitSHA className="sha" commit={selectedCommits[0]} />
|
<TooltippedCommitSHA
|
||||||
|
className="selectable"
|
||||||
|
commit={selectedCommits[0]}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -505,7 +507,7 @@ export class CommitSummary extends React.Component<
|
||||||
title="Diff Options"
|
title="Diff Options"
|
||||||
>
|
>
|
||||||
<DiffOptions
|
<DiffOptions
|
||||||
sourceTab={RepositorySectionTab.History}
|
isInteractiveDiff={false}
|
||||||
hideWhitespaceChanges={this.props.hideWhitespaceInDiff}
|
hideWhitespaceChanges={this.props.hideWhitespaceInDiff}
|
||||||
onHideWhitespaceChangesChanged={
|
onHideWhitespaceChangesChanged={
|
||||||
this.props.onHideWhitespaceInDiffChanged
|
this.props.onHideWhitespaceInDiffChanged
|
||||||
|
@ -642,7 +644,7 @@ export class CommitSummary extends React.Component<
|
||||||
<Octicon symbol={OcticonSymbol.tag} />
|
<Octicon symbol={OcticonSymbol.tag} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="tags">{tags.join(', ')}</span>
|
<span className="tags selectable">{tags.join(', ')}</span>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,25 +96,24 @@ export class MergeCallToActionWithConflicts extends React.Component<
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private onOperationChange = (option: IDropdownSelectButtonOption) => {
|
private onOperationChange = (
|
||||||
const value = option.value as MultiCommitOperationKind
|
option: IDropdownSelectButtonOption<MultiCommitOperationKind>
|
||||||
this.setState({ selectedOperation: value })
|
) => {
|
||||||
if (value === MultiCommitOperationKind.Rebase) {
|
this.setState({ selectedOperation: option.value })
|
||||||
|
if (option.value === MultiCommitOperationKind.Rebase) {
|
||||||
this.updateRebasePreview(this.props.comparisonBranch)
|
this.updateRebasePreview(this.props.comparisonBranch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onOperationInvoked = async (
|
private onOperationInvoked = async (
|
||||||
event: React.MouseEvent<HTMLButtonElement>,
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
selectedOption: IDropdownSelectButtonOption
|
selectedOption: IDropdownSelectButtonOption<MultiCommitOperationKind>
|
||||||
) => {
|
) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
const { dispatcher, repository } = this.props
|
const { dispatcher, repository } = this.props
|
||||||
|
|
||||||
await this.dispatchOperation(
|
await this.dispatchOperation(selectedOption.value)
|
||||||
selectedOption.value as MultiCommitOperationKind
|
|
||||||
)
|
|
||||||
|
|
||||||
dispatcher.executeCompare(repository, {
|
dispatcher.executeCompare(repository, {
|
||||||
kind: HistoryTabMode.History,
|
kind: HistoryTabMode.History,
|
||||||
|
|
|
@ -168,8 +168,8 @@ const sendErrorWithContext = (
|
||||||
extra.windowZoomFactor = `${currentState.windowZoomFactor}`
|
extra.windowZoomFactor = `${currentState.windowZoomFactor}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentState.errors.length > 0) {
|
if (currentState.errorCount > 0) {
|
||||||
extra.activeAppErrors = `${currentState.errors.length}`
|
extra.activeAppErrors = `${currentState.errorCount}`
|
||||||
}
|
}
|
||||||
|
|
||||||
extra.repositoryCount = `${currentState.repositories.length}`
|
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')
|
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 {
|
private canDragCommit(event: React.MouseEvent<HTMLDivElement>): boolean {
|
||||||
// right clicks or shift clicks
|
|
||||||
const isSpecialClick =
|
const isSpecialClick =
|
||||||
event.button === 2 ||
|
event.button !== 0 ||
|
||||||
(__DARWIN__ && event.button === 0 && event.ctrlKey) ||
|
(__DARWIN__ && event.button === 0 && event.ctrlKey) ||
|
||||||
event.shiftKey
|
event.shiftKey
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ interface IListRowProps {
|
||||||
readonly selected?: boolean
|
readonly selected?: boolean
|
||||||
|
|
||||||
/** callback to fire when the DOM element is created */
|
/** callback to fire when the DOM element is created */
|
||||||
readonly onRef?: (element: HTMLDivElement | null) => void
|
readonly onRowRef?: (index: number, element: HTMLDivElement | null) => void
|
||||||
|
|
||||||
/** callback to fire when the row receives a mouseover event */
|
/** callback to fire when the row receives a mouseover event */
|
||||||
readonly onRowMouseOver: (index: number, e: React.MouseEvent<any>) => void
|
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 */
|
/** callback to fire when the row receives a keyboard event */
|
||||||
readonly onRowKeyDown: (index: number, e: React.KeyboardEvent<any>) => void
|
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
|
* Whether or not this list row is going to be selectable either through
|
||||||
* keyboard navigation, pointer clicks, or both. This is used to determine
|
* keyboard navigation, pointer clicks, or both. This is used to determine
|
||||||
|
@ -53,6 +65,10 @@ interface IListRowProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ListRow extends React.Component<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>) => {
|
private onRowMouseOver = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
this.props.onRowMouseOver(this.props.rowIndex, e)
|
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)
|
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() {
|
public render() {
|
||||||
const selected = this.props.selected
|
const selected = this.props.selected
|
||||||
const className = classNames(
|
const className = classNames(
|
||||||
|
@ -102,13 +126,15 @@ export class ListRow extends React.Component<IListRowProps, {}> {
|
||||||
role={role}
|
role={role}
|
||||||
className={className}
|
className={className}
|
||||||
tabIndex={this.props.tabIndex}
|
tabIndex={this.props.tabIndex}
|
||||||
ref={this.props.onRef}
|
ref={this.onRef}
|
||||||
onMouseOver={this.onRowMouseOver}
|
onMouseOver={this.onRowMouseOver}
|
||||||
onMouseDown={this.onRowMouseDown}
|
onMouseDown={this.onRowMouseDown}
|
||||||
onMouseUp={this.onRowMouseUp}
|
onMouseUp={this.onRowMouseUp}
|
||||||
onClick={this.onRowClick}
|
onClick={this.onRowClick}
|
||||||
onKeyDown={this.onRowKeyDown}
|
onKeyDown={this.onRowKeyDown}
|
||||||
style={style}
|
style={style}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -269,6 +269,8 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
private fakeScroll: HTMLDivElement | null = null
|
private fakeScroll: HTMLDivElement | null = null
|
||||||
private focusRow = -1
|
private focusRow = -1
|
||||||
|
|
||||||
|
private readonly rowRefs = new Map<number, HTMLDivElement>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The style prop for our child Grid. We keep this here in order
|
* 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
|
* 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>) => {
|
private toggleSelection = (event: React.KeyboardEvent<any>) => {
|
||||||
this.props.selectedRows.forEach(row => {
|
this.props.selectedRows.forEach(row => {
|
||||||
if (!this.props.onRowClick) {
|
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>) => {
|
private onRowMouseOver = (row: number, event: React.MouseEvent<any>) => {
|
||||||
if (this.props.selectOnHover && this.canSelectRow(row)) {
|
if (this.props.selectOnHover && this.canSelectRow(row)) {
|
||||||
if (
|
if (!this.props.selectedRows.includes(row)) {
|
||||||
this.props.selectedRows.includes(row) &&
|
this.props.onSelectionChanged?.([row], { kind: 'hover', event })
|
||||||
this.props.onSelectionChanged
|
|
||||||
) {
|
|
||||||
this.props.onSelectionChanged([row], { kind: 'hover', event })
|
|
||||||
// By calling scrollRowToVisible we ensure that hovering over a partially
|
// 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
|
// visible item at the top or bottom of the list scrolls it into view but
|
||||||
// more importantly `scrollRowToVisible` automatically manages focus so
|
// more importantly `scrollRowToVisible` automatically manages focus so
|
||||||
// using it here allows us to piggy-back on its focus-preserving magic
|
// using it here allows us to piggy-back on its focus-preserving magic
|
||||||
// even though we could theoretically live without scrolling
|
// 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)
|
this.scrollRowToVisible(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
private scrollRowToVisible(row: number) {
|
private scrollRowToVisible(row: number, moveFocus = true) {
|
||||||
if (this.grid !== null) {
|
if (this.grid !== null) {
|
||||||
this.grid.scrollToCell({ rowIndex: row, columnIndex: 0 })
|
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) => {
|
private onRowRef = (rowIndex: number, element: HTMLDivElement | null) => {
|
||||||
if (this.props.focusOnHover !== false && element !== null) {
|
if (element === null) {
|
||||||
element.focus()
|
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) => {
|
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 selected = this.props.selectedRows.indexOf(rowIndex) !== -1
|
||||||
const customClasses = this.getCustomRowClassNames(rowIndex)
|
const customClasses = this.getCustomRowClassNames(rowIndex)
|
||||||
|
|
||||||
const focused = rowIndex === this.focusRow
|
|
||||||
|
|
||||||
// An unselectable row shouldn't be focusable
|
// An unselectable row shouldn't be focusable
|
||||||
let tabIndex: number | undefined = undefined
|
let tabIndex: number | undefined = undefined
|
||||||
if (selectable) {
|
if (selectable) {
|
||||||
tabIndex = selected && this.props.selectedRows[0] === rowIndex ? 0 : -1
|
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 row = this.props.rowRenderer(rowIndex)
|
||||||
|
|
||||||
const element =
|
const element =
|
||||||
|
@ -870,7 +902,7 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
<ListRow
|
<ListRow
|
||||||
key={params.key}
|
key={params.key}
|
||||||
id={id}
|
id={id}
|
||||||
onRef={ref}
|
onRowRef={this.onRowRef}
|
||||||
rowCount={this.props.rowCount}
|
rowCount={this.props.rowCount}
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
|
@ -880,6 +912,8 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
onRowMouseDown={this.onRowMouseDown}
|
onRowMouseDown={this.onRowMouseDown}
|
||||||
onRowMouseUp={this.onRowMouseUp}
|
onRowMouseUp={this.onRowMouseUp}
|
||||||
onRowMouseOver={this.onRowMouseOver}
|
onRowMouseOver={this.onRowMouseOver}
|
||||||
|
onRowFocus={this.onRowFocus}
|
||||||
|
onRowBlur={this.onRowBlur}
|
||||||
style={params.style}
|
style={params.style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
children={element}
|
children={element}
|
||||||
|
@ -978,6 +1012,7 @@ export class List extends React.Component<IListProps, IListState> {
|
||||||
<FocusContainer
|
<FocusContainer
|
||||||
className="list-focus-container"
|
className="list-focus-container"
|
||||||
onKeyDown={this.onFocusContainerKeyDown}
|
onKeyDown={this.onFocusContainerKeyDown}
|
||||||
|
onFocusWithinChanged={this.onFocusWithinChanged}
|
||||||
>
|
>
|
||||||
<Grid
|
<Grid
|
||||||
aria-label={''}
|
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) => {
|
private onTargetMouseEnter = (event: MouseEvent) => {
|
||||||
|
this.updateMouseRect(event)
|
||||||
|
|
||||||
this.mouseOverTarget = true
|
this.mouseOverTarget = true
|
||||||
this.cancelHideTooltip()
|
this.cancelHideTooltip()
|
||||||
if (!this.state.show) {
|
if (!this.state.show) {
|
||||||
|
@ -308,7 +314,7 @@ export class Tooltip<T extends TooltipTarget> extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
private onTargetMouseMove = (event: MouseEvent) => {
|
private onTargetMouseMove = (event: MouseEvent) => {
|
||||||
this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20)
|
this.updateMouseRect(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onTargetMouseDown = (event: MouseEvent) => {
|
private onTargetMouseDown = (event: MouseEvent) => {
|
||||||
|
|
|
@ -7,7 +7,9 @@ import { RebasePreview } from '../../models/rebase'
|
||||||
import { Repository } from '../../models/repository'
|
import { Repository } from '../../models/repository'
|
||||||
import { IDropdownSelectButtonOption } from '../dropdown-select-button'
|
import { IDropdownSelectButtonOption } from '../dropdown-select-button'
|
||||||
|
|
||||||
export function getMergeOptions(): ReadonlyArray<IDropdownSelectButtonOption> {
|
export function getMergeOptions(): ReadonlyArray<
|
||||||
|
IDropdownSelectButtonOption<MultiCommitOperationKind>
|
||||||
|
> {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Create a merge commit',
|
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 */
|
/** Tell the main process to quit the app and install updates */
|
||||||
export const quitAndInstallUpdate = sendProxy('quit-and-install-updates', 0)
|
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 */
|
/** Subscribes to auto updater error events originating from the main process */
|
||||||
export function onAutoUpdaterError(
|
export function onAutoUpdaterError(
|
||||||
errorHandler: (evt: Electron.IpcRendererEvent, error: Error) => void
|
errorHandler: (evt: Electron.IpcRendererEvent, error: Error) => void
|
||||||
|
@ -200,6 +203,12 @@ export function onNativeThemeUpdated(eventHandler: () => void) {
|
||||||
ipcRenderer.on('native-theme-updated', eventHandler)
|
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 */
|
/** Tell the main process to set the native theme source */
|
||||||
export const setNativeThemeSource = sendProxy('set-native-theme-source', 1)
|
export const setNativeThemeSource = sendProxy('set-native-theme-source', 1)
|
||||||
|
|
||||||
|
@ -273,6 +282,29 @@ export function sendWillQuitSync() {
|
||||||
ipcRenderer.sendSync('will-quit')
|
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
|
* 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
|
return currentBranch === defaultBranch ? null : defaultBranch
|
||||||
}
|
}
|
||||||
|
|
||||||
private onOperationChange = (option: IDropdownSelectButtonOption) => {
|
private onOperationChange = (
|
||||||
const value = option.value as MultiCommitOperationKind
|
option: IDropdownSelectButtonOption<MultiCommitOperationKind>
|
||||||
|
) => {
|
||||||
const { dispatcher, repository } = this.props
|
const { dispatcher, repository } = this.props
|
||||||
const { selectedBranch } = this.state
|
const { selectedBranch } = this.state
|
||||||
switch (value) {
|
switch (option.value) {
|
||||||
case MultiCommitOperationKind.Merge:
|
case MultiCommitOperationKind.Merge:
|
||||||
dispatcher.startMergeBranchOperation(repository, false, selectedBranch)
|
dispatcher.startMergeBranchOperation(repository, false, selectedBranch)
|
||||||
break
|
break
|
||||||
|
@ -179,7 +180,7 @@ export abstract class BaseChooseBranchDialog extends React.Component<
|
||||||
case MultiCommitOperationKind.Reorder:
|
case MultiCommitOperationKind.Reorder:
|
||||||
break
|
break
|
||||||
default:
|
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 * 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 { Branch } from '../../models/branch'
|
||||||
|
import { ImageDiffType } from '../../models/diff'
|
||||||
import { Repository } from '../../models/repository'
|
import { Repository } from '../../models/repository'
|
||||||
import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog'
|
import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog'
|
||||||
import { Dispatcher } from '../dispatcher'
|
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 { 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 {
|
interface IOpenPullRequestDialogProps {
|
||||||
readonly repository: Repository
|
readonly repository: Repository
|
||||||
|
@ -26,14 +34,42 @@ interface IOpenPullRequestDialogProps {
|
||||||
readonly defaultBranch: Branch | null
|
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 */
|
/** Called to dismiss the dialog */
|
||||||
readonly onDismissed: () => void
|
readonly onDismissed: () => void
|
||||||
|
@ -42,9 +78,24 @@ interface IOpenPullRequestDialogProps {
|
||||||
/** The component for start a pull request. */
|
/** The component for start a pull request. */
|
||||||
export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialogProps> {
|
export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialogProps> {
|
||||||
private onCreatePullRequest = () => {
|
private onCreatePullRequest = () => {
|
||||||
this.props.dispatcher.createPullRequest(this.props.repository)
|
const { currentBranchHasPullRequest, dispatcher, repository, onDismissed } =
|
||||||
// TODO: create pr from dialog pr stat?
|
this.props
|
||||||
this.props.dispatcher.recordCreatePullRequest()
|
|
||||||
|
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() {
|
private renderHeader() {
|
||||||
|
@ -52,8 +103,8 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
||||||
currentBranch,
|
currentBranch,
|
||||||
pullRequestState,
|
pullRequestState,
|
||||||
defaultBranch,
|
defaultBranch,
|
||||||
allBranches,
|
prBaseBranches,
|
||||||
recentBranches,
|
prRecentBaseBranches,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { baseBranch, commitSHAs } = pullRequestState
|
const { baseBranch, commitSHAs } = pullRequestState
|
||||||
return (
|
return (
|
||||||
|
@ -61,25 +112,152 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
||||||
baseBranch={baseBranch}
|
baseBranch={baseBranch}
|
||||||
currentBranch={currentBranch}
|
currentBranch={currentBranch}
|
||||||
defaultBranch={defaultBranch}
|
defaultBranch={defaultBranch}
|
||||||
allBranches={allBranches}
|
prBaseBranches={prBaseBranches}
|
||||||
recentBranches={recentBranches}
|
prRecentBaseBranches={prRecentBaseBranches}
|
||||||
commitCount={commitSHAs?.length ?? 0}
|
commitCount={commitSHAs?.length ?? 0}
|
||||||
|
onBranchChange={this.onBranchChange}
|
||||||
onDismissed={this.props.onDismissed}
|
onDismissed={this.props.onDismissed}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderContent() {
|
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() {
|
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 (
|
return (
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
<PullRequestMergeStatus mergeStatus={mergeStatus} />
|
||||||
|
|
||||||
<OkCancelButtonGroup
|
<OkCancelButtonGroup
|
||||||
okButtonText="Create Pull Request"
|
okButtonText={okButton}
|
||||||
okButtonTitle="Create pull request on GitHub."
|
okButtonTitle={buttonTitle}
|
||||||
cancelButtonText="Cancel"
|
cancelButtonText="Cancel"
|
||||||
|
okButtonDisabled={commitSHAs === null || commitSHAs.length === 0}
|
||||||
/>
|
/>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
)
|
)
|
||||||
|
@ -93,8 +271,7 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
||||||
onDismissed={this.props.onDismissed}
|
onDismissed={this.props.onDismissed}
|
||||||
>
|
>
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
<div className="content">{this.renderContent()}</div>
|
{this.renderContent()}
|
||||||
|
|
||||||
{this.renderFooter()}
|
{this.renderFooter()}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,12 +2,12 @@ import * as React from 'react'
|
||||||
import { Branch } from '../../models/branch'
|
import { Branch } from '../../models/branch'
|
||||||
import { BranchSelect } from '../branches/branch-select'
|
import { BranchSelect } from '../branches/branch-select'
|
||||||
import { DialogHeader } from '../dialog/header'
|
import { DialogHeader } from '../dialog/header'
|
||||||
import { createUniqueId } from '../lib/id-pool'
|
import { createUniqueId, releaseUniqueId } from '../lib/id-pool'
|
||||||
import { Ref } from '../lib/ref'
|
import { Ref } from '../lib/ref'
|
||||||
|
|
||||||
interface IOpenPullRequestDialogHeaderProps {
|
interface IOpenPullRequestDialogHeaderProps {
|
||||||
/** The base branch of the pull request */
|
/** The base branch of the pull request */
|
||||||
readonly baseBranch: Branch
|
readonly baseBranch: Branch | null
|
||||||
|
|
||||||
/** The branch of the pull request */
|
/** The branch of the pull request */
|
||||||
readonly currentBranch: Branch
|
readonly currentBranch: Branch
|
||||||
|
@ -18,18 +18,27 @@ interface IOpenPullRequestDialogHeaderProps {
|
||||||
readonly defaultBranch: Branch | null
|
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 */
|
/** The count of commits of the pull request */
|
||||||
readonly commitCount: number
|
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
|
* Event triggered when the dialog is dismissed by the user in the
|
||||||
* ways described in the dismissable prop.
|
* ways described in the dismissable prop.
|
||||||
|
@ -37,23 +46,43 @@ interface IOpenPullRequestDialogHeaderProps {
|
||||||
readonly onDismissed?: () => void
|
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
|
* 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.
|
* base branch dropdown and merge details common to all pull request views.
|
||||||
*/
|
*/
|
||||||
export class OpenPullRequestDialogHeader extends React.Component<
|
export class OpenPullRequestDialogHeader extends React.Component<
|
||||||
IOpenPullRequestDialogHeaderProps,
|
IOpenPullRequestDialogHeaderProps,
|
||||||
{}
|
IOpenPullRequestDialogHeaderState
|
||||||
> {
|
> {
|
||||||
|
public constructor(props: IOpenPullRequestDialogHeaderProps) {
|
||||||
|
super(props)
|
||||||
|
this.state = { titleId: createUniqueId(`Dialog_Open_Pull_Request`) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
releaseUniqueId(this.state.titleId)
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const title = __DARWIN__ ? 'Open a Pull Request' : 'Open a pull request'
|
const title = __DARWIN__ ? 'Open a Pull Request' : 'Open a pull request'
|
||||||
const {
|
const {
|
||||||
baseBranch,
|
baseBranch,
|
||||||
currentBranch,
|
currentBranch,
|
||||||
defaultBranch,
|
defaultBranch,
|
||||||
allBranches,
|
prBaseBranches,
|
||||||
recentBranches,
|
prRecentBaseBranches,
|
||||||
commitCount,
|
commitCount,
|
||||||
|
onBranchChange,
|
||||||
onDismissed,
|
onDismissed,
|
||||||
} = this.props
|
} = this.props
|
||||||
const commits = `${commitCount} commit${commitCount > 1 ? 's' : ''}`
|
const commits = `${commitCount} commit${commitCount > 1 ? 's' : ''}`
|
||||||
|
@ -61,7 +90,7 @@ export class OpenPullRequestDialogHeader extends React.Component<
|
||||||
return (
|
return (
|
||||||
<DialogHeader
|
<DialogHeader
|
||||||
title={title}
|
title={title}
|
||||||
titleId={createUniqueId(`Dialog_${title}_${title}`)}
|
titleId={this.state.titleId}
|
||||||
dismissable={true}
|
dismissable={true}
|
||||||
onDismissed={onDismissed}
|
onDismissed={onDismissed}
|
||||||
>
|
>
|
||||||
|
@ -72,8 +101,15 @@ export class OpenPullRequestDialogHeader extends React.Component<
|
||||||
branch={baseBranch}
|
branch={baseBranch}
|
||||||
defaultBranch={defaultBranch}
|
defaultBranch={defaultBranch}
|
||||||
currentBranch={currentBranch}
|
currentBranch={currentBranch}
|
||||||
allBranches={allBranches}
|
allBranches={prBaseBranches}
|
||||||
recentBranches={recentBranches}
|
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>.
|
from <Ref>{currentBranch.name}</Ref>.
|
||||||
</div>
|
</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 confirmRepositoryRemoval: boolean
|
||||||
readonly confirmDiscardChanges: boolean
|
readonly confirmDiscardChanges: boolean
|
||||||
readonly confirmDiscardChangesPermanently: boolean
|
readonly confirmDiscardChangesPermanently: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
readonly confirmForcePush: boolean
|
readonly confirmForcePush: boolean
|
||||||
readonly confirmUndoCommit: boolean
|
readonly confirmUndoCommit: boolean
|
||||||
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
||||||
|
@ -79,6 +80,7 @@ interface IPreferencesState {
|
||||||
readonly confirmRepositoryRemoval: boolean
|
readonly confirmRepositoryRemoval: boolean
|
||||||
readonly confirmDiscardChanges: boolean
|
readonly confirmDiscardChanges: boolean
|
||||||
readonly confirmDiscardChangesPermanently: boolean
|
readonly confirmDiscardChangesPermanently: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
readonly confirmForcePush: boolean
|
readonly confirmForcePush: boolean
|
||||||
readonly confirmUndoCommit: boolean
|
readonly confirmUndoCommit: boolean
|
||||||
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
||||||
|
@ -121,6 +123,7 @@ export class Preferences extends React.Component<
|
||||||
confirmRepositoryRemoval: false,
|
confirmRepositoryRemoval: false,
|
||||||
confirmDiscardChanges: false,
|
confirmDiscardChanges: false,
|
||||||
confirmDiscardChangesPermanently: false,
|
confirmDiscardChangesPermanently: false,
|
||||||
|
confirmDiscardStash: false,
|
||||||
confirmForcePush: false,
|
confirmForcePush: false,
|
||||||
confirmUndoCommit: false,
|
confirmUndoCommit: false,
|
||||||
uncommittedChangesStrategy: defaultUncommittedChangesStrategy,
|
uncommittedChangesStrategy: defaultUncommittedChangesStrategy,
|
||||||
|
@ -178,6 +181,7 @@ export class Preferences extends React.Component<
|
||||||
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
||||||
confirmDiscardChangesPermanently:
|
confirmDiscardChangesPermanently:
|
||||||
this.props.confirmDiscardChangesPermanently,
|
this.props.confirmDiscardChangesPermanently,
|
||||||
|
confirmDiscardStash: this.props.confirmDiscardStash,
|
||||||
confirmForcePush: this.props.confirmForcePush,
|
confirmForcePush: this.props.confirmForcePush,
|
||||||
confirmUndoCommit: this.props.confirmUndoCommit,
|
confirmUndoCommit: this.props.confirmUndoCommit,
|
||||||
uncommittedChangesStrategy: this.props.uncommittedChangesStrategy,
|
uncommittedChangesStrategy: this.props.uncommittedChangesStrategy,
|
||||||
|
@ -333,12 +337,14 @@ export class Preferences extends React.Component<
|
||||||
confirmDiscardChangesPermanently={
|
confirmDiscardChangesPermanently={
|
||||||
this.state.confirmDiscardChangesPermanently
|
this.state.confirmDiscardChangesPermanently
|
||||||
}
|
}
|
||||||
|
confirmDiscardStash={this.state.confirmDiscardStash}
|
||||||
confirmForcePush={this.state.confirmForcePush}
|
confirmForcePush={this.state.confirmForcePush}
|
||||||
confirmUndoCommit={this.state.confirmUndoCommit}
|
confirmUndoCommit={this.state.confirmUndoCommit}
|
||||||
onConfirmRepositoryRemovalChanged={
|
onConfirmRepositoryRemovalChanged={
|
||||||
this.onConfirmRepositoryRemovalChanged
|
this.onConfirmRepositoryRemovalChanged
|
||||||
}
|
}
|
||||||
onConfirmDiscardChangesChanged={this.onConfirmDiscardChangesChanged}
|
onConfirmDiscardChangesChanged={this.onConfirmDiscardChangesChanged}
|
||||||
|
onConfirmDiscardStashChanged={this.onConfirmDiscardStashChanged}
|
||||||
onConfirmForcePushChanged={this.onConfirmForcePushChanged}
|
onConfirmForcePushChanged={this.onConfirmForcePushChanged}
|
||||||
onConfirmDiscardChangesPermanentlyChanged={
|
onConfirmDiscardChangesPermanentlyChanged={
|
||||||
this.onConfirmDiscardChangesPermanentlyChanged
|
this.onConfirmDiscardChangesPermanentlyChanged
|
||||||
|
@ -410,6 +416,10 @@ export class Preferences extends React.Component<
|
||||||
this.setState({ confirmDiscardChanges: value })
|
this.setState({ confirmDiscardChanges: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onConfirmDiscardStashChanged = (value: boolean) => {
|
||||||
|
this.setState({ confirmDiscardStash: value })
|
||||||
|
}
|
||||||
|
|
||||||
private onConfirmDiscardChangesPermanentlyChanged = (value: boolean) => {
|
private onConfirmDiscardChangesPermanentlyChanged = (value: boolean) => {
|
||||||
this.setState({ confirmDiscardChangesPermanently: value })
|
this.setState({ confirmDiscardChangesPermanently: value })
|
||||||
}
|
}
|
||||||
|
@ -562,6 +572,10 @@ export class Preferences extends React.Component<
|
||||||
this.state.confirmForcePush
|
this.state.confirmForcePush
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await this.props.dispatcher.setConfirmDiscardStashSetting(
|
||||||
|
this.state.confirmDiscardStash
|
||||||
|
)
|
||||||
|
|
||||||
await this.props.dispatcher.setConfirmUndoCommitSetting(
|
await this.props.dispatcher.setConfirmUndoCommitSetting(
|
||||||
this.state.confirmUndoCommit
|
this.state.confirmUndoCommit
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,10 +6,12 @@ interface IPromptsPreferencesProps {
|
||||||
readonly confirmRepositoryRemoval: boolean
|
readonly confirmRepositoryRemoval: boolean
|
||||||
readonly confirmDiscardChanges: boolean
|
readonly confirmDiscardChanges: boolean
|
||||||
readonly confirmDiscardChangesPermanently: boolean
|
readonly confirmDiscardChangesPermanently: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
readonly confirmForcePush: boolean
|
readonly confirmForcePush: boolean
|
||||||
readonly confirmUndoCommit: boolean
|
readonly confirmUndoCommit: boolean
|
||||||
readonly onConfirmDiscardChangesChanged: (checked: boolean) => void
|
readonly onConfirmDiscardChangesChanged: (checked: boolean) => void
|
||||||
readonly onConfirmDiscardChangesPermanentlyChanged: (checked: boolean) => void
|
readonly onConfirmDiscardChangesPermanentlyChanged: (checked: boolean) => void
|
||||||
|
readonly onConfirmDiscardStashChanged: (checked: boolean) => void
|
||||||
readonly onConfirmRepositoryRemovalChanged: (checked: boolean) => void
|
readonly onConfirmRepositoryRemovalChanged: (checked: boolean) => void
|
||||||
readonly onConfirmForcePushChanged: (checked: boolean) => void
|
readonly onConfirmForcePushChanged: (checked: boolean) => void
|
||||||
readonly onConfirmUndoCommitChanged: (checked: boolean) => void
|
readonly onConfirmUndoCommitChanged: (checked: boolean) => void
|
||||||
|
@ -19,6 +21,7 @@ interface IPromptsPreferencesState {
|
||||||
readonly confirmRepositoryRemoval: boolean
|
readonly confirmRepositoryRemoval: boolean
|
||||||
readonly confirmDiscardChanges: boolean
|
readonly confirmDiscardChanges: boolean
|
||||||
readonly confirmDiscardChangesPermanently: boolean
|
readonly confirmDiscardChangesPermanently: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
readonly confirmForcePush: boolean
|
readonly confirmForcePush: boolean
|
||||||
readonly confirmUndoCommit: boolean
|
readonly confirmUndoCommit: boolean
|
||||||
}
|
}
|
||||||
|
@ -35,6 +38,7 @@ export class Prompts extends React.Component<
|
||||||
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
||||||
confirmDiscardChangesPermanently:
|
confirmDiscardChangesPermanently:
|
||||||
this.props.confirmDiscardChangesPermanently,
|
this.props.confirmDiscardChangesPermanently,
|
||||||
|
confirmDiscardStash: this.props.confirmDiscardStash,
|
||||||
confirmForcePush: this.props.confirmForcePush,
|
confirmForcePush: this.props.confirmForcePush,
|
||||||
confirmUndoCommit: this.props.confirmUndoCommit,
|
confirmUndoCommit: this.props.confirmUndoCommit,
|
||||||
}
|
}
|
||||||
|
@ -58,6 +62,15 @@ export class Prompts extends React.Component<
|
||||||
this.props.onConfirmDiscardChangesPermanentlyChanged(value)
|
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 = (
|
private onConfirmForcePushChanged = (
|
||||||
event: React.FormEvent<HTMLInputElement>
|
event: React.FormEvent<HTMLInputElement>
|
||||||
) => {
|
) => {
|
||||||
|
@ -116,6 +129,15 @@ export class Prompts extends React.Component<
|
||||||
}
|
}
|
||||||
onChange={this.onConfirmDiscardChangesPermanentlyChanged}
|
onChange={this.onConfirmDiscardChangesPermanentlyChanged}
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Discarding stash"
|
||||||
|
value={
|
||||||
|
this.state.confirmDiscardStash
|
||||||
|
? CheckboxValue.On
|
||||||
|
: CheckboxValue.Off
|
||||||
|
}
|
||||||
|
onChange={this.onConfirmDiscardStashChanged}
|
||||||
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Force pushing"
|
label="Force pushing"
|
||||||
value={
|
value={
|
||||||
|
|
|
@ -193,10 +193,14 @@ const renderAheadBehindIndicator = (aheadBehind: IAheadBehind) => {
|
||||||
'its tracked branch.'
|
'its tracked branch.'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ahead-behind" title={aheadBehindTooltip}>
|
<TooltippedContent
|
||||||
|
className="ahead-behind"
|
||||||
|
tagName="div"
|
||||||
|
tooltip={aheadBehindTooltip}
|
||||||
|
>
|
||||||
{ahead > 0 && <Octicon symbol={OcticonSymbol.arrowUp} />}
|
{ahead > 0 && <Octicon symbol={OcticonSymbol.arrowUp} />}
|
||||||
{behind > 0 && <Octicon symbol={OcticonSymbol.arrowDown} />}
|
{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 { dragAndDropManager } from '../lib/drag-and-drop-manager'
|
||||||
import { DragType } from '../models/drag-drop'
|
import { DragType } from '../models/drag-drop'
|
||||||
import { clamp } from '../lib/clamp'
|
import { clamp } from '../lib/clamp'
|
||||||
|
import { PullRequestSuggestedNextAction } from '../models/pull-request'
|
||||||
|
|
||||||
interface IRepositoryViewProps {
|
interface IRepositoryViewProps {
|
||||||
readonly repository: Repository
|
readonly repository: Repository
|
||||||
|
@ -49,6 +50,7 @@ interface IRepositoryViewProps {
|
||||||
readonly hideWhitespaceInHistoryDiff: boolean
|
readonly hideWhitespaceInHistoryDiff: boolean
|
||||||
readonly showSideBySideDiff: boolean
|
readonly showSideBySideDiff: boolean
|
||||||
readonly askForConfirmationOnDiscardChanges: boolean
|
readonly askForConfirmationOnDiscardChanges: boolean
|
||||||
|
readonly askForConfirmationOnDiscardStash: boolean
|
||||||
readonly focusCommitMessage: boolean
|
readonly focusCommitMessage: boolean
|
||||||
readonly commitSpellcheckEnabled: boolean
|
readonly commitSpellcheckEnabled: boolean
|
||||||
readonly accounts: ReadonlyArray<Account>
|
readonly accounts: ReadonlyArray<Account>
|
||||||
|
@ -91,6 +93,9 @@ interface IRepositoryViewProps {
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
commits: ReadonlyArray<CommitOneLine>
|
commits: ReadonlyArray<CommitOneLine>
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
|
/** The user's preference of pull request suggested next action to use **/
|
||||||
|
readonly pullRequestSuggestedNextAction?: PullRequestSuggestedNextAction
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRepositoryViewState {
|
interface IRepositoryViewState {
|
||||||
|
@ -350,6 +355,9 @@ export class RepositoryView extends React.Component<
|
||||||
fileListWidth={this.props.stashedFilesWidth}
|
fileListWidth={this.props.stashedFilesWidth}
|
||||||
repository={this.props.repository}
|
repository={this.props.repository}
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
|
askForConfirmationOnDiscardStash={
|
||||||
|
this.props.askForConfirmationOnDiscardStash
|
||||||
|
}
|
||||||
isWorkingTreeClean={isWorkingTreeClean}
|
isWorkingTreeClean={isWorkingTreeClean}
|
||||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||||
onOpenBinaryFile={this.onOpenBinaryFile}
|
onOpenBinaryFile={this.onOpenBinaryFile}
|
||||||
|
@ -461,6 +469,9 @@ export class RepositoryView extends React.Component<
|
||||||
this.props.externalEditorLabel !== undefined
|
this.props.externalEditorLabel !== undefined
|
||||||
}
|
}
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
|
pullRequestSuggestedNextAction={
|
||||||
|
this.props.pullRequestSuggestedNextAction
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,19 @@ import { Dispatcher } from '../dispatcher'
|
||||||
import { Row } from '../lib/row'
|
import { Row } from '../lib/row'
|
||||||
import { IStashEntry } from '../../models/stash-entry'
|
import { IStashEntry } from '../../models/stash-entry'
|
||||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||||
|
import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
||||||
|
|
||||||
interface IConfirmDiscardStashProps {
|
interface IConfirmDiscardStashProps {
|
||||||
readonly dispatcher: Dispatcher
|
readonly dispatcher: Dispatcher
|
||||||
readonly repository: Repository
|
readonly repository: Repository
|
||||||
readonly stash: IStashEntry
|
readonly stash: IStashEntry
|
||||||
|
readonly askForConfirmationOnDiscardStash: boolean
|
||||||
readonly onDismissed: () => void
|
readonly onDismissed: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IConfirmDiscardStashState {
|
interface IConfirmDiscardStashState {
|
||||||
readonly isDiscarding: boolean
|
readonly isDiscarding: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Dialog to confirm dropping a stash
|
* Dialog to confirm dropping a stash
|
||||||
|
@ -28,6 +31,7 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isDiscarding: false,
|
isDiscarding: false,
|
||||||
|
confirmDiscardStash: props.askForConfirmationOnDiscardStash,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +50,17 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Row>Are you sure you want to discard these stashed changes?</Row>
|
<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>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<OkCancelButtonGroup destructive={true} okButtonText="Discard" />
|
<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 () => {
|
private onSubmit = async () => {
|
||||||
const { dispatcher, repository, stash, onDismissed } = this.props
|
const { dispatcher, repository, stash, onDismissed } = this.props
|
||||||
|
|
||||||
|
@ -62,6 +85,7 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
dispatcher.setConfirmDiscardStashSetting(this.state.confirmDiscardStash)
|
||||||
await dispatcher.dropStash(repository, stash)
|
await dispatcher.dropStash(repository, stash)
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -11,11 +11,13 @@ interface IStashDiffHeaderProps {
|
||||||
readonly stashEntry: IStashEntry
|
readonly stashEntry: IStashEntry
|
||||||
readonly repository: Repository
|
readonly repository: Repository
|
||||||
readonly dispatcher: Dispatcher
|
readonly dispatcher: Dispatcher
|
||||||
|
readonly askForConfirmationOnDiscardStash: boolean
|
||||||
readonly isWorkingTreeClean: boolean
|
readonly isWorkingTreeClean: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IStashDiffHeaderState {
|
interface IStashDiffHeaderState {
|
||||||
readonly isRestoring: boolean
|
readonly isRestoring: boolean
|
||||||
|
readonly isDiscarding: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,12 +33,13 @@ export class StashDiffHeader extends React.Component<
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRestoring: false,
|
isRestoring: false,
|
||||||
|
isDiscarding: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { isWorkingTreeClean } = this.props
|
const { isWorkingTreeClean } = this.props
|
||||||
const { isRestoring } = this.state
|
const { isRestoring, isDiscarding } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header">
|
<div className="header">
|
||||||
|
@ -44,10 +47,12 @@ export class StashDiffHeader extends React.Component<
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<OkCancelButtonGroup
|
<OkCancelButtonGroup
|
||||||
okButtonText="Restore"
|
okButtonText="Restore"
|
||||||
okButtonDisabled={isRestoring || !isWorkingTreeClean}
|
okButtonDisabled={
|
||||||
|
isRestoring || !isWorkingTreeClean || isDiscarding
|
||||||
|
}
|
||||||
onOkButtonClick={this.onRestoreClick}
|
onOkButtonClick={this.onRestoreClick}
|
||||||
cancelButtonText="Discard"
|
cancelButtonText="Discard"
|
||||||
cancelButtonDisabled={isRestoring}
|
cancelButtonDisabled={isRestoring || isDiscarding}
|
||||||
onCancelButtonClick={this.onDiscardClick}
|
onCancelButtonClick={this.onDiscardClick}
|
||||||
/>
|
/>
|
||||||
{this.renderExplanatoryText()}
|
{this.renderExplanatoryText()}
|
||||||
|
@ -80,13 +85,33 @@ export class StashDiffHeader extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDiscardClick = () => {
|
private onDiscardClick = async () => {
|
||||||
const { dispatcher, repository, stashEntry } = this.props
|
const {
|
||||||
dispatcher.showPopup({
|
dispatcher,
|
||||||
type: PopupType.ConfirmDiscardStash,
|
|
||||||
stash: stashEntry,
|
|
||||||
repository,
|
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 () => {
|
private onRestoreClick = async () => {
|
||||||
|
|
|
@ -27,6 +27,9 @@ interface IStashDiffViewerProps {
|
||||||
readonly repository: Repository
|
readonly repository: Repository
|
||||||
readonly dispatcher: Dispatcher
|
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. */
|
/** Whether we should display side by side diffs. */
|
||||||
readonly showSideBySideDiff: boolean
|
readonly showSideBySideDiff: boolean
|
||||||
|
|
||||||
|
@ -113,6 +116,9 @@ export class StashDiffViewer extends React.PureComponent<IStashDiffViewerProps>
|
||||||
repository={repository}
|
repository={repository}
|
||||||
dispatcher={dispatcher}
|
dispatcher={dispatcher}
|
||||||
isWorkingTreeClean={isWorkingTreeClean}
|
isWorkingTreeClean={isWorkingTreeClean}
|
||||||
|
askForConfirmationOnDiscardStash={
|
||||||
|
this.props.askForConfirmationOnDiscardStash
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="commit-details">
|
<div className="commit-details">
|
||||||
<Resizable
|
<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/_pull-request-quick-view';
|
||||||
@import 'ui/discard-changes-retry';
|
@import 'ui/discard-changes-retry';
|
||||||
@import 'ui/_git-email-not-found-warning';
|
@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/ci-check-run-rerun';
|
||||||
@import 'dialogs/unreachable-commits';
|
@import 'dialogs/unreachable-commits';
|
||||||
@import 'dialogs/open-pull-request';
|
@import 'dialogs/open-pull-request';
|
||||||
|
@import 'dialogs/installing-update';
|
||||||
|
|
||||||
// The styles herein attempt to follow a flow where margins are only applied
|
// 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
|
// 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>
|
// allow easy layout using generalized components and elements such as <Row>
|
||||||
// and <p>.
|
// and <p>.
|
||||||
dialog {
|
dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
overflow: unset;
|
overflow: unset;
|
||||||
// These are the 24px versions of the alert and stop octicons
|
// These are the 24px versions of the alert and stop octicons
|
||||||
// from oction v10.0.0
|
// from oction v10.0.0
|
||||||
|
@ -125,14 +129,28 @@ dialog {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not([open]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
// The dialog embeds a fieldset as the first child of the form element
|
// 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
|
// in order to be able to disable all form elements and buttons in one
|
||||||
// swoop. This resets all styles for that fieldset.
|
// swoop. This resets all styles for that fieldset.
|
||||||
& > form > fieldset {
|
& > form {
|
||||||
border: 0;
|
min-height: 0;
|
||||||
margin: 0;
|
height: 100%;
|
||||||
padding: 0;
|
& > fieldset {
|
||||||
min-width: 0;
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-header {
|
.dialog-header {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
.dropdown-select-button {
|
.dropdown-select-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.dropdown-button-wrappers {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
&.open-bottom {
|
&.open-bottom {
|
||||||
.invoke-button {
|
.invoke-button {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
|
@ -34,19 +38,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoke-button {
|
.invoke-button {
|
||||||
width: 88%;
|
|
||||||
display: inline;
|
display: inline;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
float: left;
|
|
||||||
height: 30px;
|
height: 30px;
|
||||||
// counter balances center for the 12% dropdown button
|
flex-grow: 1;
|
||||||
padding-left: 12% !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-button {
|
.dropdown-button {
|
||||||
width: 12%;
|
min-width: 30px;
|
||||||
padding: var(--spacing-half);
|
padding: var(--spacing-half);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
|
@ -85,29 +86,50 @@
|
||||||
box-shadow: var(--base-box-shadow);
|
box-shadow: var(--base-box-shadow);
|
||||||
width: 99.9%;
|
width: 99.9%;
|
||||||
|
|
||||||
ul {
|
padding: 0;
|
||||||
padding: 0;
|
margin: 0;
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
li {
|
:first-child.button-component {
|
||||||
list-style-type: none;
|
border-top: 1px solid var(--box-border-color);
|
||||||
padding: var(--spacing) var(--spacing-double);
|
}
|
||||||
padding-left: var(--spacing-triple);
|
|
||||||
border-bottom: 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 {
|
.option-description {
|
||||||
color: var(--text-secondary-color);
|
color: var(--box-selected-active-text-color);
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-option-indicator {
|
|
||||||
position: absolute;
|
|
||||||
left: var(--spacing);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
li:hover {
|
.option-description {
|
||||||
background-color: var(--box-selected-background-color);
|
color: var(--text-secondary-color);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-option-indicator {
|
||||||
|
position: absolute;
|
||||||
|
left: 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);
|
background: var(--list-item-selected-active-badge-background-color);
|
||||||
color: var(--list-item-selected-active-badge-color);
|
color: var(--list-item-selected-active-badge-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.change-indicator-wrapper {
|
||||||
|
.octicon {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -314,6 +314,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unified-diff {
|
&.unified-diff {
|
||||||
|
--line-gutter-right-border-width: 4px;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
.before,
|
.before,
|
||||||
.after {
|
.after {
|
||||||
|
@ -323,12 +325,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.hunk-handle {
|
.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 {
|
&.hunk-info {
|
||||||
background: var(--diff-hunk-gutter-background-color);
|
.line-number {
|
||||||
border-color: var(--diff-hunk-border-color);
|
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 {
|
.line-number {
|
||||||
|
@ -349,8 +366,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.editable .row .line-number {
|
&.editable .row {
|
||||||
border-right-width: 4px;
|
.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;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pull-request-action {
|
||||||
|
.dropdown-select-button {
|
||||||
|
.invoke-button {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Lessen the padding at 1.5x zoom and above **/
|
/** Lessen the padding at 1.5x zoom and above **/
|
||||||
@media screen and (max-width: 640px) {
|
@media screen and (max-width: 640px) {
|
||||||
padding: var(--spacing-double);
|
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 {
|
.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 {
|
header.dialog-header {
|
||||||
padding-bottom: var(--spacing);
|
padding-bottom: var(--spacing);
|
||||||
|
|
||||||
|
@ -15,4 +20,35 @@
|
||||||
padding: var(--spacing-half);
|
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
|
vertical-align: bottom; // For some reason, `bottom` places the text in the middle
|
||||||
}
|
}
|
||||||
|
|
||||||
.sha {
|
.selectable {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ body > .tooltip,
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
background-color: var(--tooltip-background-color);
|
background-color: var(--tooltip-background-color);
|
||||||
|
|
|
@ -575,9 +575,38 @@ describe('git/diff', () => {
|
||||||
'feature-branch',
|
'feature-branch',
|
||||||
'irrelevantToTest'
|
'irrelevantToTest'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expect(changesetData).not.toBeNull()
|
||||||
|
if (changesetData === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
expect(changesetData.files).toHaveLength(1)
|
expect(changesetData.files).toHaveLength(1)
|
||||||
expect(changesetData.files[0].path).toBe('feature.md')
|
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', () => {
|
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"
|
regenerator-runtime "^0.11.0"
|
||||||
|
|
||||||
balanced-match@^1.0.0:
|
balanced-match@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
bl@^3.0.0:
|
bl@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
|
@ -131,9 +131,9 @@ bl@^3.0.0:
|
||||||
readable-stream "^3.0.1"
|
readable-stream "^3.0.1"
|
||||||
|
|
||||||
brace-expansion@^1.1.7:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.8"
|
version "1.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||||
integrity sha1-wHshHHyVLsH479Uad+8NHTmQopI=
|
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match "^1.0.0"
|
balanced-match "^1.0.0"
|
||||||
concat-map "0.0.1"
|
concat-map "0.0.1"
|
||||||
|
@ -243,7 +243,7 @@ compare-versions@^3.6.0:
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
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:
|
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
|
@ -854,9 +854,9 @@ mimic-response@^3.1.0:
|
||||||
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
||||||
|
|
||||||
minimatch@^3.0.4:
|
minimatch@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,53 @@
|
||||||
"[Fixed] Close 'Resolve conflicts before Rebase' dialog will not disable menu items - #13081. Thanks @angusdev!",
|
"[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"
|
"[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": ["[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": [
|
"3.1.1": [
|
||||||
"[Fixed] App correctly remembers undo commit prompt setting - #15408"
|
"[Fixed] App correctly remembers undo commit prompt setting - #15408"
|
||||||
],
|
],
|
||||||
|
|
|
@ -19,13 +19,13 @@ versions look similar to the below output:
|
||||||
|
|
||||||
```shellsession
|
```shellsession
|
||||||
$ node -v
|
$ node -v
|
||||||
v10.15.4
|
v16.13.0
|
||||||
|
|
||||||
$ yarn -v
|
$ yarn -v
|
||||||
1.15.2
|
1.21.1
|
||||||
|
|
||||||
$ python --version
|
$ python --version
|
||||||
Python 2.7.15
|
Python 3.9.x
|
||||||
```
|
```
|
||||||
|
|
||||||
There are also [additional resources](tooling.md) to configure your favorite
|
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.
|
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.
|
3. Under **Add a personal access token** choose a name and set an expiration date for your token.
|
||||||
|
|
||||||
|
|
|
@ -307,6 +307,9 @@ These editors are currently supported:
|
||||||
- [Neovim](https://neovim.io/)
|
- [Neovim](https://neovim.io/)
|
||||||
- [Code](https://github.com/elementary/code)
|
- [Code](https://github.com/elementary/code)
|
||||||
- [Lite XL](https://lite-xl.com/)
|
- [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:
|
These are defined in a list at the top of the file:
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
"eslint-plugin-json": "^2.1.1",
|
"eslint-plugin-json": "^2.1.1",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
"eslint-plugin-react": "7.26.1",
|
"eslint-plugin-react": "7.26.1",
|
||||||
"express": "^4.15.0",
|
"express": "^4.17.3",
|
||||||
"fake-indexeddb": "^2.0.4",
|
"fake-indexeddb": "^2.0.4",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"front-matter": "^2.3.0",
|
"front-matter": "^2.3.0",
|
||||||
|
|
|
@ -102,7 +102,11 @@ function packageWindows() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldMakeDelta()) {
|
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()) {
|
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"
|
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
||||||
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
|
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
|
||||||
|
|
||||||
accepts@~1.3.7:
|
accepts@~1.3.8:
|
||||||
version "1.3.7"
|
version "1.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
||||||
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
|
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types "~2.1.24"
|
mime-types "~2.1.34"
|
||||||
negotiator "0.6.2"
|
negotiator "0.6.3"
|
||||||
|
|
||||||
acorn-globals@^6.0.0:
|
acorn-globals@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
|
@ -2445,11 +2445,6 @@ bcrypt-pbkdf@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tweetnacl "^0.14.3"
|
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:
|
big.js@^5.2.2:
|
||||||
version "5.2.2"
|
version "5.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
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"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
body-parser@1.19.0:
|
body-parser@1.19.2:
|
||||||
version "1.19.0"
|
version "1.19.2"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e"
|
||||||
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
|
integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes "3.1.0"
|
bytes "3.1.2"
|
||||||
content-type "~1.0.4"
|
content-type "~1.0.4"
|
||||||
debug "2.6.9"
|
debug "2.6.9"
|
||||||
depd "~1.1.2"
|
depd "~1.1.2"
|
||||||
http-errors "1.7.2"
|
http-errors "1.8.1"
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "0.4.24"
|
||||||
on-finished "~2.3.0"
|
on-finished "~2.3.0"
|
||||||
qs "6.7.0"
|
qs "6.9.7"
|
||||||
raw-body "2.4.0"
|
raw-body "2.4.3"
|
||||||
type-is "~1.6.17"
|
type-is "~1.6.18"
|
||||||
|
|
||||||
boolbase@^1.0.0:
|
boolbase@^1.0.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
|
||||||
integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
|
integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
|
||||||
|
|
||||||
bytes@3.1.0:
|
bytes@3.1.2:
|
||||||
version "3.1.0"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||||
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
|
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||||
|
|
||||||
cache-base@^1.0.1:
|
cache-base@^1.0.1:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/console-polyfill/-/console-polyfill-0.3.0.tgz#84900902a18c47a5eba932be75fa44d23e8af861"
|
||||||
integrity sha512-w+JSDZS7XML43Xnwo2x5O5vxB0ID7T5BdqDtyqT6uiCAX2kZAgcWxNaGqT97tZfSHzfOcvrfsDAodKcJ3UvnXQ==
|
integrity sha512-w+JSDZS7XML43Xnwo2x5O5vxB0ID7T5BdqDtyqT6uiCAX2kZAgcWxNaGqT97tZfSHzfOcvrfsDAodKcJ3UvnXQ==
|
||||||
|
|
||||||
content-disposition@0.5.3:
|
content-disposition@0.5.4:
|
||||||
version "0.5.3"
|
version "0.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
|
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
|
||||||
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
|
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "5.1.2"
|
safe-buffer "5.2.1"
|
||||||
|
|
||||||
content-type@~1.0.4:
|
content-type@~1.0.4:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||||
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
|
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
|
||||||
|
|
||||||
cookie@0.4.0:
|
cookie@0.4.2:
|
||||||
version "0.4.0"
|
version "0.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
|
||||||
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
|
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
|
||||||
|
|
||||||
copy-descriptor@^0.1.0:
|
copy-descriptor@^0.1.0:
|
||||||
version "0.1.1"
|
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:
|
dependencies:
|
||||||
ms "2.0.0"
|
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"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.1.2"
|
ms "2.1.2"
|
||||||
|
|
||||||
debug@^3.1.0:
|
debug@^3.1.0, debug@^3.2.7:
|
||||||
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:
|
|
||||||
version "3.2.7"
|
version "3.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
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:
|
debuglog@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
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==
|
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
|
||||||
|
|
||||||
decode-uri-component@^0.2.0:
|
decode-uri-component@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
|
||||||
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
|
integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
|
||||||
|
|
||||||
decompress-response@^3.3.0:
|
decompress-response@^3.3.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
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:
|
emojis-list@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
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-message-util "^26.6.2"
|
||||||
jest-regex-util "^26.0.0"
|
jest-regex-util "^26.0.0"
|
||||||
|
|
||||||
express@^4.15.0:
|
express@^4.17.3:
|
||||||
version "4.17.0"
|
version "4.17.3"
|
||||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.0.tgz#288af62228a73f4c8ea2990ba3b791bb87cd4438"
|
resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1"
|
||||||
integrity sha512-1Z7/t3Z5ZnBG252gKUPyItc4xdeaA0X934ca2ewckAsVsw9EG71i++ZHZPYnus8g/s5Bty8IMpSVEuRkmwwPRQ==
|
integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==
|
||||||
dependencies:
|
dependencies:
|
||||||
accepts "~1.3.7"
|
accepts "~1.3.8"
|
||||||
array-flatten "1.1.1"
|
array-flatten "1.1.1"
|
||||||
body-parser "1.19.0"
|
body-parser "1.19.2"
|
||||||
content-disposition "0.5.3"
|
content-disposition "0.5.4"
|
||||||
content-type "~1.0.4"
|
content-type "~1.0.4"
|
||||||
cookie "0.4.0"
|
cookie "0.4.2"
|
||||||
cookie-signature "1.0.6"
|
cookie-signature "1.0.6"
|
||||||
debug "2.6.9"
|
debug "2.6.9"
|
||||||
depd "~1.1.2"
|
depd "~1.1.2"
|
||||||
|
@ -4370,13 +4325,13 @@ express@^4.15.0:
|
||||||
on-finished "~2.3.0"
|
on-finished "~2.3.0"
|
||||||
parseurl "~1.3.3"
|
parseurl "~1.3.3"
|
||||||
path-to-regexp "0.1.7"
|
path-to-regexp "0.1.7"
|
||||||
proxy-addr "~2.0.5"
|
proxy-addr "~2.0.7"
|
||||||
qs "6.7.0"
|
qs "6.9.7"
|
||||||
range-parser "~1.2.1"
|
range-parser "~1.2.1"
|
||||||
safe-buffer "5.1.2"
|
safe-buffer "5.2.1"
|
||||||
send "0.17.1"
|
send "0.17.2"
|
||||||
serve-static "1.14.1"
|
serve-static "1.14.2"
|
||||||
setprototypeof "1.1.1"
|
setprototypeof "1.2.0"
|
||||||
statuses "~1.5.0"
|
statuses "~1.5.0"
|
||||||
type-is "~1.6.18"
|
type-is "~1.6.18"
|
||||||
utils-merge "1.0.1"
|
utils-merge "1.0.1"
|
||||||
|
@ -4634,10 +4589,10 @@ form-data@~2.3.2:
|
||||||
combined-stream "1.0.6"
|
combined-stream "1.0.6"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
forwarded@~0.1.2:
|
forwarded@0.2.0:
|
||||||
version "0.1.2"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||||
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
|
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||||
|
|
||||||
fragment-cache@^0.2.1:
|
fragment-cache@^0.2.1:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||||
|
|
||||||
http-errors@1.7.2, http-errors@~1.7.2:
|
http-errors@1.8.1:
|
||||||
version "1.7.2"
|
version "1.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
|
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
|
||||||
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
|
integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==
|
||||||
dependencies:
|
dependencies:
|
||||||
depd "~1.1.2"
|
depd "~1.1.2"
|
||||||
inherits "2.0.3"
|
inherits "2.0.4"
|
||||||
setprototypeof "1.1.1"
|
setprototypeof "1.2.0"
|
||||||
statuses ">= 1.5.0 < 2"
|
statuses ">= 1.5.0 < 2"
|
||||||
toidentifier "1.0.0"
|
toidentifier "1.0.1"
|
||||||
|
|
||||||
http-proxy-agent@^4.0.1:
|
http-proxy-agent@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
|
@ -5342,16 +5297,11 @@ inflight@^1.0.4:
|
||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
wrappy "1"
|
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"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
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:
|
ini@^1.3.4:
|
||||||
version "1.3.8"
|
version "1.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
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"
|
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0"
|
||||||
integrity sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA=
|
integrity sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA=
|
||||||
|
|
||||||
ipaddr.js@1.9.0:
|
ipaddr.js@1.9.1:
|
||||||
version "1.9.0"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
|
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||||
integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
|
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||||
|
|
||||||
is-accessor-descriptor@^0.1.6:
|
is-accessor-descriptor@^0.1.6:
|
||||||
version "0.1.6"
|
version "0.1.6"
|
||||||
|
@ -6459,15 +6409,10 @@ json5@2.x, json5@^2.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
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:
|
json5@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||||
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
|
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
|
|
||||||
|
@ -6691,13 +6636,13 @@ loader-runner@^4.2.0:
|
||||||
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
|
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
|
||||||
|
|
||||||
loader-utils@^1.1.0:
|
loader-utils@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
|
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3"
|
||||||
integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=
|
integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==
|
||||||
dependencies:
|
dependencies:
|
||||||
big.js "^3.1.3"
|
big.js "^5.2.2"
|
||||||
emojis-list "^2.0.0"
|
emojis-list "^3.0.0"
|
||||||
json5 "^0.5.0"
|
json5 "^1.0.1"
|
||||||
|
|
||||||
loader-utils@^2.0.0:
|
loader-utils@^2.0.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397"
|
||||||
integrity sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==
|
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"
|
version "2.1.35"
|
||||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
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"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||||
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
|
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
|
||||||
|
|
||||||
minimist@^1.1.1, minimist@^1.2.0:
|
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
||||||
version "1.2.0"
|
version "1.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||||
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
|
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
||||||
|
|
||||||
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==
|
|
||||||
|
|
||||||
mixin-deep@^1.2.0:
|
mixin-deep@^1.2.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
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:
|
ms@2.1.2:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||||
|
|
||||||
ms@^2.1.1:
|
ms@2.1.3, ms@^2.1.1:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
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"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||||
|
|
||||||
negotiator@0.6.2:
|
negotiator@0.6.3:
|
||||||
version "0.6.2"
|
version "0.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||||
|
|
||||||
neo-async@^2.6.2:
|
neo-async@^2.6.2:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||||
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
|
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
|
||||||
|
|
||||||
proxy-addr@~2.0.5:
|
proxy-addr@~2.0.7:
|
||||||
version "2.0.5"
|
version "2.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
|
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
||||||
integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==
|
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded "~0.1.2"
|
forwarded "0.2.0"
|
||||||
ipaddr.js "1.9.0"
|
ipaddr.js "1.9.1"
|
||||||
|
|
||||||
prr@~0.0.0:
|
prr@~0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
|
@ -8030,15 +7960,15 @@ pupa@^2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
escape-goat "^2.0.0"
|
escape-goat "^2.0.0"
|
||||||
|
|
||||||
qs@6.7.0:
|
qs@6.9.7:
|
||||||
version "6.7.0"
|
version "6.9.7"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
|
||||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==
|
||||||
|
|
||||||
qs@~6.5.2:
|
qs@~6.5.2:
|
||||||
version "6.5.2"
|
version "6.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
|
||||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
|
||||||
|
|
||||||
querystring@^0.2.0:
|
querystring@^0.2.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||||
|
|
||||||
raw-body@2.4.0:
|
raw-body@2.4.3:
|
||||||
version "2.4.0"
|
version "2.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c"
|
||||||
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
|
integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes "3.1.0"
|
bytes "3.1.2"
|
||||||
http-errors "1.7.2"
|
http-errors "1.8.1"
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "0.4.24"
|
||||||
unpipe "1.0.0"
|
unpipe "1.0.0"
|
||||||
|
|
||||||
|
@ -8531,21 +8461,16 @@ run-parallel@^1.1.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask "^1.2.2"
|
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:
|
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.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:
|
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
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:
|
safe-regex@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
|
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
|
||||||
|
@ -8686,10 +8611,10 @@ semver@^7.3.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^6.0.0"
|
lru-cache "^6.0.0"
|
||||||
|
|
||||||
send@0.17.1:
|
send@0.17.2:
|
||||||
version "0.17.1"
|
version "0.17.2"
|
||||||
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
|
resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820"
|
||||||
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
|
integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "2.6.9"
|
debug "2.6.9"
|
||||||
depd "~1.1.2"
|
depd "~1.1.2"
|
||||||
|
@ -8698,9 +8623,9 @@ send@0.17.1:
|
||||||
escape-html "~1.0.3"
|
escape-html "~1.0.3"
|
||||||
etag "~1.8.1"
|
etag "~1.8.1"
|
||||||
fresh "0.5.2"
|
fresh "0.5.2"
|
||||||
http-errors "~1.7.2"
|
http-errors "1.8.1"
|
||||||
mime "1.6.0"
|
mime "1.6.0"
|
||||||
ms "2.1.1"
|
ms "2.1.3"
|
||||||
on-finished "~2.3.0"
|
on-finished "~2.3.0"
|
||||||
range-parser "~1.2.1"
|
range-parser "~1.2.1"
|
||||||
statuses "~1.5.0"
|
statuses "~1.5.0"
|
||||||
|
@ -8719,15 +8644,15 @@ serialize-javascript@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
randombytes "^2.1.0"
|
randombytes "^2.1.0"
|
||||||
|
|
||||||
serve-static@1.14.1:
|
serve-static@1.14.2:
|
||||||
version "1.14.1"
|
version "1.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
|
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa"
|
||||||
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
|
integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
encodeurl "~1.0.2"
|
encodeurl "~1.0.2"
|
||||||
escape-html "~1.0.3"
|
escape-html "~1.0.3"
|
||||||
parseurl "~1.3.3"
|
parseurl "~1.3.3"
|
||||||
send "0.17.1"
|
send "0.17.2"
|
||||||
|
|
||||||
set-blocking@^2.0.0:
|
set-blocking@^2.0.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||||
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
|
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
|
||||||
|
|
||||||
setprototypeof@1.1.1:
|
setprototypeof@1.2.0:
|
||||||
version "1.1.1"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||||
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
|
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||||
|
|
||||||
shallow-clone@^3.0.0:
|
shallow-clone@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
|
@ -9487,10 +9412,10 @@ to-space-case@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-no-case "^1.0.0"
|
to-no-case "^1.0.0"
|
||||||
|
|
||||||
toidentifier@1.0.0:
|
toidentifier@1.0.1:
|
||||||
version "1.0.0"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||||
|
|
||||||
totalist@^1.0.0:
|
totalist@^1.0.0:
|
||||||
version "1.1.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"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||||
|
|
||||||
type-is@~1.6.17, type-is@~1.6.18:
|
type-is@~1.6.18:
|
||||||
version "1.6.18"
|
version "1.6.18"
|
||||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||||
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||||
|
|
Loading…
Reference in a new issue