mirror of
https://github.com/desktop/desktop
synced 2024-08-28 04:31:14 +00:00
Merge branch 'development' into releases/3.1.5
This commit is contained in:
commit
c6e0865d6a
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -57,6 +57,8 @@ jobs:
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.arch }}
|
npm_config_arch: ${{ matrix.arch }}
|
||||||
TARGET_ARCH: ${{ matrix.arch }}
|
TARGET_ARCH: ${{ matrix.arch }}
|
||||||
|
- name: Validate Electron version
|
||||||
|
run: yarn run validate-electron-version
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
- name: Validate changelog
|
- name: Validate changelog
|
||||||
|
|
|
@ -327,6 +327,7 @@ export enum FoldoutType {
|
||||||
Branch,
|
Branch,
|
||||||
AppMenu,
|
AppMenu,
|
||||||
AddMenu,
|
AddMenu,
|
||||||
|
PushPull,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppMenuFoldout = {
|
export type AppMenuFoldout = {
|
||||||
|
@ -358,6 +359,7 @@ export type Foldout =
|
||||||
| { type: FoldoutType.AddMenu }
|
| { type: FoldoutType.AddMenu }
|
||||||
| BranchFoldout
|
| BranchFoldout
|
||||||
| AppMenuFoldout
|
| AppMenuFoldout
|
||||||
|
| { type: FoldoutType.PushPull }
|
||||||
|
|
||||||
export enum RepositorySectionTab {
|
export enum RepositorySectionTab {
|
||||||
Changes,
|
Changes,
|
||||||
|
|
|
@ -91,6 +91,10 @@ const editors: IDarwinExternalEditor[] = [
|
||||||
name: 'WebStorm',
|
name: 'WebStorm',
|
||||||
bundleIdentifiers: ['com.jetbrains.WebStorm'],
|
bundleIdentifiers: ['com.jetbrains.WebStorm'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'CLion',
|
||||||
|
bundleIdentifiers: ['com.jetbrains.CLion'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Typora',
|
name: 'Typora',
|
||||||
bundleIdentifiers: ['abnerworks.Typora'],
|
bundleIdentifiers: ['abnerworks.Typora'],
|
||||||
|
|
|
@ -59,7 +59,7 @@ type WindowsExternalEditor = {
|
||||||
readonly displayNamePrefix: string
|
readonly displayNamePrefix: string
|
||||||
|
|
||||||
/** Value of the Publisher registry key that belongs to this editor. */
|
/** Value of the Publisher registry key that belongs to this editor. */
|
||||||
readonly publisher: string
|
readonly publishers: string[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default shell script name for JetBrains Product
|
* Default shell script name for JetBrains Product
|
||||||
|
@ -150,21 +150,21 @@ const editors: WindowsExternalEditor[] = [
|
||||||
registryKeys: [CurrentUserUninstallKey('atom')],
|
registryKeys: [CurrentUserUninstallKey('atom')],
|
||||||
executableShimPaths: [['bin', 'atom.cmd']],
|
executableShimPaths: [['bin', 'atom.cmd']],
|
||||||
displayNamePrefix: 'Atom',
|
displayNamePrefix: 'Atom',
|
||||||
publisher: 'GitHub Inc.',
|
publishers: ['GitHub Inc.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Atom Beta',
|
name: 'Atom Beta',
|
||||||
registryKeys: [CurrentUserUninstallKey('atom-beta')],
|
registryKeys: [CurrentUserUninstallKey('atom-beta')],
|
||||||
executableShimPaths: [['bin', 'atom-beta.cmd']],
|
executableShimPaths: [['bin', 'atom-beta.cmd']],
|
||||||
displayNamePrefix: 'Atom Beta',
|
displayNamePrefix: 'Atom Beta',
|
||||||
publisher: 'GitHub Inc.',
|
publishers: ['GitHub Inc.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Atom Nightly',
|
name: 'Atom Nightly',
|
||||||
registryKeys: [CurrentUserUninstallKey('atom-nightly')],
|
registryKeys: [CurrentUserUninstallKey('atom-nightly')],
|
||||||
executableShimPaths: [['bin', 'atom-nightly.cmd']],
|
executableShimPaths: [['bin', 'atom-nightly.cmd']],
|
||||||
displayNamePrefix: 'Atom Nightly',
|
displayNamePrefix: 'Atom Nightly',
|
||||||
publisher: 'GitHub Inc.',
|
publishers: ['GitHub Inc.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Visual Studio Code',
|
name: 'Visual Studio Code',
|
||||||
|
@ -186,7 +186,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
],
|
],
|
||||||
executableShimPaths: [['bin', 'code.cmd']],
|
executableShimPaths: [['bin', 'code.cmd']],
|
||||||
displayNamePrefix: 'Microsoft Visual Studio Code',
|
displayNamePrefix: 'Microsoft Visual Studio Code',
|
||||||
publisher: 'Microsoft Corporation',
|
publishers: ['Microsoft Corporation'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Visual Studio Code (Insiders)',
|
name: 'Visual Studio Code (Insiders)',
|
||||||
|
@ -208,29 +208,63 @@ const editors: WindowsExternalEditor[] = [
|
||||||
],
|
],
|
||||||
executableShimPaths: [['bin', 'code-insiders.cmd']],
|
executableShimPaths: [['bin', 'code-insiders.cmd']],
|
||||||
displayNamePrefix: 'Microsoft Visual Studio Code Insiders',
|
displayNamePrefix: 'Microsoft Visual Studio Code Insiders',
|
||||||
publisher: 'Microsoft Corporation',
|
publishers: ['Microsoft Corporation'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Visual Studio Codium',
|
name: 'Visual Studio Codium',
|
||||||
registryKeys: [
|
registryKeys: [
|
||||||
// 64-bit version of VSCodium (user)
|
// 64-bit version of VSCodium (user)
|
||||||
CurrentUserUninstallKey('{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1'),
|
CurrentUserUninstallKey('{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1'),
|
||||||
// 32-bit version of VSCodium (user)
|
// 32-bit version of VSCodium (user) - new key
|
||||||
|
CurrentUserUninstallKey('{0FD05EB4-651E-4E78-A062-515204B47A3A}_is1'),
|
||||||
|
// ARM64 version of VSCodium (user) - new key
|
||||||
|
CurrentUserUninstallKey('{57FD70A5-1B8D-4875-9F40-C5553F094828}_is1'),
|
||||||
|
// 64-bit version of VSCodium (system) - new key
|
||||||
|
LocalMachineUninstallKey('{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1'),
|
||||||
|
// 32-bit version of VSCodium (system) - new key
|
||||||
|
Wow64LocalMachineUninstallKey(
|
||||||
|
'{763CBF88-25C6-4B10-952F-326AE657F16B}_is1'
|
||||||
|
),
|
||||||
|
// ARM64 version of VSCodium (system) - new key
|
||||||
|
LocalMachineUninstallKey('{67DEE444-3D04-4258-B92A-BC1F0FF2CAE4}_is1'),
|
||||||
|
// 32-bit version of VSCodium (user) - old key
|
||||||
CurrentUserUninstallKey('{C6065F05-9603-4FC4-8101-B9781A25D88E}}_is1'),
|
CurrentUserUninstallKey('{C6065F05-9603-4FC4-8101-B9781A25D88E}}_is1'),
|
||||||
// ARM64 version of VSCodium (user)
|
// ARM64 version of VSCodium (user) - old key
|
||||||
CurrentUserUninstallKey('{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}_is1'),
|
CurrentUserUninstallKey('{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}_is1'),
|
||||||
// 64-bit version of VSCodium (system)
|
// 64-bit version of VSCodium (system) - old key
|
||||||
LocalMachineUninstallKey('{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}_is1'),
|
LocalMachineUninstallKey('{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}_is1'),
|
||||||
// 32-bit version of VSCodium (system)
|
// 32-bit version of VSCodium (system) - old key
|
||||||
Wow64LocalMachineUninstallKey(
|
Wow64LocalMachineUninstallKey(
|
||||||
'{E34003BB-9E10-4501-8C11-BE3FAA83F23F}_is1'
|
'{E34003BB-9E10-4501-8C11-BE3FAA83F23F}_is1'
|
||||||
),
|
),
|
||||||
// ARM64 version of VSCodium (system)
|
// ARM64 version of VSCodium (system) - old key
|
||||||
LocalMachineUninstallKey('{D1ACE434-89C5-48D1-88D3-E2991DF85475}_is1'),
|
LocalMachineUninstallKey('{D1ACE434-89C5-48D1-88D3-E2991DF85475}_is1'),
|
||||||
],
|
],
|
||||||
executableShimPaths: [['bin', 'codium.cmd']],
|
executableShimPaths: [['bin', 'codium.cmd']],
|
||||||
displayNamePrefix: 'VSCodium',
|
displayNamePrefix: 'VSCodium',
|
||||||
publisher: 'Microsoft Corporation',
|
publishers: ['VSCodium', 'Microsoft Corporation'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Visual Studio Codium (Insiders)',
|
||||||
|
registryKeys: [
|
||||||
|
// 64-bit version of VSCodium - Insiders (user)
|
||||||
|
CurrentUserUninstallKey('{20F79D0D-A9AC-4220-9A81-CE675FFB6B41}_is1'),
|
||||||
|
// 32-bit version of VSCodium - Insiders (user)
|
||||||
|
CurrentUserUninstallKey('{ED2E5618-3E7E-4888-BF3C-A6CCC84F586F}_is1'),
|
||||||
|
// ARM64 version of VSCodium - Insiders (user)
|
||||||
|
CurrentUserUninstallKey('{2E362F92-14EA-455A-9ABD-3E656BBBFE71}_is1'),
|
||||||
|
// 64-bit version of VSCodium - Insiders (system)
|
||||||
|
LocalMachineUninstallKey('{B2E0DDB2-120E-4D34-9F7E-8C688FF839A2}_is1'),
|
||||||
|
// 32-bit version of VSCodium - Insiders (system)
|
||||||
|
Wow64LocalMachineUninstallKey(
|
||||||
|
'{EF35BB36-FA7E-4BB9-B7DA-D1E09F2DA9C9}_is1'
|
||||||
|
),
|
||||||
|
// ARM64 version of VSCodium - Insiders (system)
|
||||||
|
LocalMachineUninstallKey('{44721278-64C6-4513-BC45-D48E07830599}_is1'),
|
||||||
|
],
|
||||||
|
executableShimPaths: [['bin', 'codium-insiders.cmd']],
|
||||||
|
displayNamePrefix: 'VSCodium (Insiders)',
|
||||||
|
publishers: ['VSCodium'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sublime Text',
|
name: 'Sublime Text',
|
||||||
|
@ -242,7 +276,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
],
|
],
|
||||||
executableShimPaths: [['subl.exe']],
|
executableShimPaths: [['subl.exe']],
|
||||||
displayNamePrefix: 'Sublime Text',
|
displayNamePrefix: 'Sublime Text',
|
||||||
publisher: 'Sublime HQ Pty Ltd',
|
publishers: ['Sublime HQ Pty Ltd'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Brackets',
|
name: 'Brackets',
|
||||||
|
@ -251,7 +285,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
],
|
],
|
||||||
executableShimPaths: [['Brackets.exe']],
|
executableShimPaths: [['Brackets.exe']],
|
||||||
displayNamePrefix: 'Brackets',
|
displayNamePrefix: 'Brackets',
|
||||||
publisher: 'brackets.io',
|
publishers: ['brackets.io'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ColdFusion Builder',
|
name: 'ColdFusion Builder',
|
||||||
|
@ -263,7 +297,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
],
|
],
|
||||||
executableShimPaths: [['CFBuilder.exe']],
|
executableShimPaths: [['CFBuilder.exe']],
|
||||||
displayNamePrefix: 'Adobe ColdFusion Builder',
|
displayNamePrefix: 'Adobe ColdFusion Builder',
|
||||||
publisher: 'Adobe Systems Incorporated',
|
publishers: ['Adobe Systems Incorporated'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Typora',
|
name: 'Typora',
|
||||||
|
@ -277,7 +311,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
],
|
],
|
||||||
executableShimPaths: [['typora.exe']],
|
executableShimPaths: [['typora.exe']],
|
||||||
displayNamePrefix: 'Typora',
|
displayNamePrefix: 'Typora',
|
||||||
publisher: 'typora.io',
|
publishers: ['typora.io'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'SlickEdit',
|
name: 'SlickEdit',
|
||||||
|
@ -307,7 +341,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
],
|
],
|
||||||
executableShimPaths: [['win', 'vs.exe']],
|
executableShimPaths: [['win', 'vs.exe']],
|
||||||
displayNamePrefix: 'SlickEdit',
|
displayNamePrefix: 'SlickEdit',
|
||||||
publisher: 'SlickEdit Inc.',
|
publishers: ['SlickEdit Inc.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Aptana Studio 3',
|
name: 'Aptana Studio 3',
|
||||||
|
@ -316,7 +350,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
],
|
],
|
||||||
executableShimPaths: [['AptanaStudio3.exe']],
|
executableShimPaths: [['AptanaStudio3.exe']],
|
||||||
displayNamePrefix: 'Aptana Studio',
|
displayNamePrefix: 'Aptana Studio',
|
||||||
publisher: 'Appcelerator',
|
publishers: ['Appcelerator'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains Webstorm',
|
name: 'JetBrains Webstorm',
|
||||||
|
@ -324,7 +358,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('webstorm'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('webstorm'),
|
||||||
jetBrainsToolboxScriptName: 'webstorm',
|
jetBrainsToolboxScriptName: 'webstorm',
|
||||||
displayNamePrefix: 'WebStorm',
|
displayNamePrefix: 'WebStorm',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains Phpstorm',
|
name: 'JetBrains Phpstorm',
|
||||||
|
@ -332,7 +366,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('phpstorm'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('phpstorm'),
|
||||||
jetBrainsToolboxScriptName: 'phpstorm',
|
jetBrainsToolboxScriptName: 'phpstorm',
|
||||||
displayNamePrefix: 'PhpStorm',
|
displayNamePrefix: 'PhpStorm',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Android Studio',
|
name: 'Android Studio',
|
||||||
|
@ -344,7 +378,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
['..', 'bin', `studio.exe`],
|
['..', 'bin', `studio.exe`],
|
||||||
],
|
],
|
||||||
displayNamePrefix: 'Android Studio',
|
displayNamePrefix: 'Android Studio',
|
||||||
publisher: 'Google LLC',
|
publishers: ['Google LLC'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Notepad++',
|
name: 'Notepad++',
|
||||||
|
@ -356,7 +390,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
],
|
],
|
||||||
installLocationRegistryKey: 'DisplayIcon',
|
installLocationRegistryKey: 'DisplayIcon',
|
||||||
displayNamePrefix: 'Notepad++',
|
displayNamePrefix: 'Notepad++',
|
||||||
publisher: 'Notepad++ Team',
|
publishers: ['Notepad++ Team'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains Rider',
|
name: 'JetBrains Rider',
|
||||||
|
@ -364,14 +398,14 @@ const editors: WindowsExternalEditor[] = [
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('rider'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('rider'),
|
||||||
jetBrainsToolboxScriptName: 'rider',
|
jetBrainsToolboxScriptName: 'rider',
|
||||||
displayNamePrefix: 'JetBrains Rider',
|
displayNamePrefix: 'JetBrains Rider',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'RStudio',
|
name: 'RStudio',
|
||||||
registryKeys: [Wow64LocalMachineUninstallKey('RStudio')],
|
registryKeys: [Wow64LocalMachineUninstallKey('RStudio')],
|
||||||
installLocationRegistryKey: 'DisplayIcon',
|
installLocationRegistryKey: 'DisplayIcon',
|
||||||
displayNamePrefix: 'RStudio',
|
displayNamePrefix: 'RStudio',
|
||||||
publisher: 'RStudio',
|
publishers: ['RStudio'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains IntelliJ Idea',
|
name: 'JetBrains IntelliJ Idea',
|
||||||
|
@ -379,7 +413,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('idea'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('idea'),
|
||||||
jetBrainsToolboxScriptName: 'idea',
|
jetBrainsToolboxScriptName: 'idea',
|
||||||
displayNamePrefix: 'IntelliJ IDEA ',
|
displayNamePrefix: 'IntelliJ IDEA ',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains IntelliJ Idea Community Edition',
|
name: 'JetBrains IntelliJ Idea Community Edition',
|
||||||
|
@ -388,7 +422,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
),
|
),
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('idea'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('idea'),
|
||||||
displayNamePrefix: 'IntelliJ IDEA Community Edition ',
|
displayNamePrefix: 'IntelliJ IDEA Community Edition ',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains PyCharm',
|
name: 'JetBrains PyCharm',
|
||||||
|
@ -396,14 +430,14 @@ const editors: WindowsExternalEditor[] = [
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('pycharm'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('pycharm'),
|
||||||
jetBrainsToolboxScriptName: 'pycharm',
|
jetBrainsToolboxScriptName: 'pycharm',
|
||||||
displayNamePrefix: 'PyCharm ',
|
displayNamePrefix: 'PyCharm ',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains PyCharm Community Edition',
|
name: 'JetBrains PyCharm Community Edition',
|
||||||
registryKeys: registryKeysForJetBrainsIDE('PyCharm Community Edition'),
|
registryKeys: registryKeysForJetBrainsIDE('PyCharm Community Edition'),
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('pycharm'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('pycharm'),
|
||||||
displayNamePrefix: 'PyCharm Community Edition',
|
displayNamePrefix: 'PyCharm Community Edition',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains CLion',
|
name: 'JetBrains CLion',
|
||||||
|
@ -411,7 +445,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('clion'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('clion'),
|
||||||
jetBrainsToolboxScriptName: 'clion',
|
jetBrainsToolboxScriptName: 'clion',
|
||||||
displayNamePrefix: 'CLion ',
|
displayNamePrefix: 'CLion ',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains RubyMine',
|
name: 'JetBrains RubyMine',
|
||||||
|
@ -419,7 +453,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('rubymine'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('rubymine'),
|
||||||
jetBrainsToolboxScriptName: 'rubymine',
|
jetBrainsToolboxScriptName: 'rubymine',
|
||||||
displayNamePrefix: 'RubyMine ',
|
displayNamePrefix: 'RubyMine ',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains GoLand',
|
name: 'JetBrains GoLand',
|
||||||
|
@ -427,7 +461,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
executableShimPaths: executableShimPathsForJetBrainsIDE('goland'),
|
executableShimPaths: executableShimPathsForJetBrainsIDE('goland'),
|
||||||
jetBrainsToolboxScriptName: 'goland',
|
jetBrainsToolboxScriptName: 'goland',
|
||||||
displayNamePrefix: 'GoLand ',
|
displayNamePrefix: 'GoLand ',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'JetBrains Fleet',
|
name: 'JetBrains Fleet',
|
||||||
|
@ -435,7 +469,7 @@ const editors: WindowsExternalEditor[] = [
|
||||||
jetBrainsToolboxScriptName: 'fleet',
|
jetBrainsToolboxScriptName: 'fleet',
|
||||||
installLocationRegistryKey: 'DisplayIcon',
|
installLocationRegistryKey: 'DisplayIcon',
|
||||||
displayNamePrefix: 'Fleet ',
|
displayNamePrefix: 'Fleet ',
|
||||||
publisher: 'JetBrains s.r.o.',
|
publishers: ['JetBrains s.r.o.'],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -471,7 +505,7 @@ async function findApplication(editor: WindowsExternalEditor) {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!displayName.startsWith(editor.displayNamePrefix) ||
|
!displayName.startsWith(editor.displayNamePrefix) ||
|
||||||
publisher !== editor.publisher
|
!editor.publishers.includes(publisher)
|
||||||
) {
|
) {
|
||||||
log.debug(`Unexpected registry entries for ${editor.name}`)
|
log.debug(`Unexpected registry entries for ${editor.name}`)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -122,3 +122,8 @@ export function enableStackedPopups(): boolean {
|
||||||
export function enablePreventClosingWhileUpdating(): boolean {
|
export function enablePreventClosingWhileUpdating(): boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Should we enable the new push-pull-fetch dropdown? */
|
||||||
|
export function enablePushPullFetchDropdown(): boolean {
|
||||||
|
return enableBetaFeatures()
|
||||||
|
}
|
||||||
|
|
|
@ -93,10 +93,7 @@ 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 {
|
import { getCurrentBranchForcePushState } from '../lib/rebase'
|
||||||
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'
|
||||||
|
@ -2800,9 +2797,15 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
remoteName = tip.branch.upstreamRemoteName
|
remoteName = tip.branch.upstreamRemoteName
|
||||||
}
|
}
|
||||||
|
|
||||||
const isForcePush =
|
const currentFoldout = this.state.currentFoldout
|
||||||
getCurrentBranchForcePushState(branchesState, aheadBehind) ===
|
|
||||||
ForcePushBranchState.Recommended
|
const isDropdownOpen =
|
||||||
|
currentFoldout !== null && currentFoldout.type === FoldoutType.PushPull
|
||||||
|
|
||||||
|
const forcePushBranchState = getCurrentBranchForcePushState(
|
||||||
|
branchesState,
|
||||||
|
aheadBehind
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PushPullButton
|
<PushPullButton
|
||||||
|
@ -2817,10 +2820,12 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
tipState={tip.kind}
|
tipState={tip.kind}
|
||||||
pullWithRebase={pullWithRebase}
|
pullWithRebase={pullWithRebase}
|
||||||
rebaseInProgress={rebaseInProgress}
|
rebaseInProgress={rebaseInProgress}
|
||||||
isForcePush={isForcePush}
|
forcePushBranchState={forcePushBranchState}
|
||||||
shouldNudge={
|
shouldNudge={
|
||||||
this.state.currentOnboardingTutorialStep === TutorialStep.PushBranch
|
this.state.currentOnboardingTutorialStep === TutorialStep.PushBranch
|
||||||
}
|
}
|
||||||
|
isDropdownOpen={isDropdownOpen}
|
||||||
|
onDropdownStateChanged={this.onPushPullDropdownStateChanged}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2884,6 +2889,14 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
this.props.dispatcher.openCreatePullRequestInBrowser(repository, branch)
|
this.props.dispatcher.openCreatePullRequestInBrowser(repository, branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onPushPullDropdownStateChanged = (newState: DropdownState) => {
|
||||||
|
if (newState === 'open') {
|
||||||
|
this.props.dispatcher.showFoldout({ type: FoldoutType.PushPull })
|
||||||
|
} else {
|
||||||
|
this.props.dispatcher.closeFoldout(FoldoutType.PushPull)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onBranchDropdownStateChanged = (newState: DropdownState) => {
|
private onBranchDropdownStateChanged = (newState: DropdownState) => {
|
||||||
if (newState === 'open') {
|
if (newState === 'open') {
|
||||||
this.props.dispatcher.showFoldout({ type: FoldoutType.Branch })
|
this.props.dispatcher.showFoldout({ type: FoldoutType.Branch })
|
||||||
|
|
|
@ -183,7 +183,7 @@ export class BranchDropdown extends React.Component<
|
||||||
|
|
||||||
const isOpen = this.props.isOpen
|
const isOpen = this.props.isOpen
|
||||||
const currentState: DropdownState = isOpen && canOpen ? 'open' : 'closed'
|
const currentState: DropdownState = isOpen && canOpen ? 'open' : 'closed'
|
||||||
const buttonClassName = classNames('nudge-arrow', {
|
const buttonClassName = classNames('branch-toolbar-button', 'nudge-arrow', {
|
||||||
'nudge-arrow-up': this.props.shouldNudge,
|
'nudge-arrow-up': this.props.shouldNudge,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,26 @@ import { TooltipTarget } from '../lib/tooltip'
|
||||||
|
|
||||||
export type DropdownState = 'open' | 'closed'
|
export type DropdownState = 'open' | 'closed'
|
||||||
|
|
||||||
|
/** Represents the style of the dropdown */
|
||||||
|
export enum ToolbarDropdownStyle {
|
||||||
|
/**
|
||||||
|
* The dropdown is rendered as a single button and, when expanded, takes the
|
||||||
|
* full height of the window.
|
||||||
|
*/
|
||||||
|
Foldout,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dropdown is rendered as two buttons: one is the toolbar button itself,
|
||||||
|
* and the other one is the expand/collapse button.
|
||||||
|
* When expanded, it only takes the height of the content.
|
||||||
|
*/
|
||||||
|
MultiOption,
|
||||||
|
}
|
||||||
|
|
||||||
export interface IToolbarDropdownProps {
|
export interface IToolbarDropdownProps {
|
||||||
|
/** The style of the dropdown. Default: Foldout */
|
||||||
|
readonly dropdownStyle?: ToolbarDropdownStyle
|
||||||
|
|
||||||
/** The primary button text, describing its function */
|
/** The primary button text, describing its function */
|
||||||
readonly title?: string
|
readonly title?: string
|
||||||
|
|
||||||
|
@ -74,6 +93,8 @@ export interface IToolbarDropdownProps {
|
||||||
*/
|
*/
|
||||||
readonly onContextMenu?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
readonly onContextMenu?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
|
|
||||||
|
readonly onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that's called whenever something is dragged over the
|
* A function that's called whenever something is dragged over the
|
||||||
* dropdown.
|
* dropdown.
|
||||||
|
@ -185,7 +206,8 @@ export class ToolbarDropdown extends React.Component<
|
||||||
IToolbarDropdownProps,
|
IToolbarDropdownProps,
|
||||||
IToolbarDropdownState
|
IToolbarDropdownState
|
||||||
> {
|
> {
|
||||||
private innerButton: ToolbarButton | null = null
|
private innerButton = React.createRef<ToolbarButton>()
|
||||||
|
private rootDiv = React.createRef<HTMLDivElement>()
|
||||||
private focusTrapOptions: FocusTrapOptions
|
private focusTrapOptions: FocusTrapOptions
|
||||||
|
|
||||||
public constructor(props: IToolbarDropdownProps) {
|
public constructor(props: IToolbarDropdownProps) {
|
||||||
|
@ -224,13 +246,25 @@ export class ToolbarDropdown extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = this.props.dropdownState
|
const state = this.props.dropdownState
|
||||||
|
const dropdownIcon = (
|
||||||
return (
|
|
||||||
<Octicon symbol={this.dropdownIcon(state)} className="dropdownArrow" />
|
<Octicon symbol={this.dropdownIcon(state)} className="dropdownArrow" />
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption ? (
|
||||||
|
<ToolbarButton
|
||||||
|
className="toolbar-dropdown-arrow-button"
|
||||||
|
onClick={this.onToggleDropdownClick}
|
||||||
|
>
|
||||||
|
{dropdownIcon}
|
||||||
|
</ToolbarButton>
|
||||||
|
) : (
|
||||||
|
dropdownIcon
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
private onToggleDropdownClick = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
const newState: DropdownState =
|
const newState: DropdownState =
|
||||||
this.props.dropdownState === 'open' ? 'closed' : 'open'
|
this.props.dropdownState === 'open' ? 'closed' : 'open'
|
||||||
|
|
||||||
|
@ -247,13 +281,22 @@ export class ToolbarDropdown extends React.Component<
|
||||||
this.props.onDropdownStateChanged(newState, source)
|
this.props.onDropdownStateChanged(newState, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onMainButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption) {
|
||||||
|
this.props.onClick?.(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onToggleDropdownClick(event)
|
||||||
|
}
|
||||||
|
|
||||||
private onContextMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
|
private onContextMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
this.props.onContextMenu?.(event)
|
this.props.onContextMenu?.(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateClientRectIfNecessary() {
|
private updateClientRectIfNecessary() {
|
||||||
if (this.props.dropdownState === 'open' && this.innerButton) {
|
if (this.props.dropdownState === 'open' && this.rootDiv.current) {
|
||||||
const newRect = this.innerButton.getButtonBoundingClientRect()
|
const newRect = this.rootDiv.current.getBoundingClientRect()
|
||||||
if (newRect) {
|
if (newRect) {
|
||||||
const currentRect = this.state.clientRect
|
const currentRect = this.state.clientRect
|
||||||
|
|
||||||
|
@ -268,10 +311,6 @@ export class ToolbarDropdown extends React.Component<
|
||||||
this.updateClientRectIfNecessary()
|
this.updateClientRectIfNecessary()
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
this.innerButton = null
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidUpdate() {
|
public componentDidUpdate() {
|
||||||
this.updateClientRectIfNecessary()
|
this.updateClientRectIfNecessary()
|
||||||
}
|
}
|
||||||
|
@ -305,12 +344,16 @@ export class ToolbarDropdown extends React.Component<
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const heightStyle: React.CSSProperties =
|
||||||
|
this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption
|
||||||
|
? { maxHeight: '100%', width: rect.width }
|
||||||
|
: { height: '100%', minWidth: rect.width }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
marginLeft: rect.left,
|
marginLeft: rect.left,
|
||||||
minWidth: rect.width,
|
|
||||||
height: '100%',
|
|
||||||
top: 0,
|
top: 0,
|
||||||
|
...heightStyle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,22 +397,21 @@ export class ToolbarDropdown extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRef = (ref: ToolbarButton | null) => {
|
|
||||||
this.innerButton = ref
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Programmatically move keyboard focus to the button element.
|
* Programmatically move keyboard focus to the button element.
|
||||||
*/
|
*/
|
||||||
public focusButton = () => {
|
public focusButton = () => {
|
||||||
if (this.innerButton) {
|
if (this.innerButton.current) {
|
||||||
this.innerButton.focusButton()
|
this.innerButton.current.focusButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const className = classNames(
|
const className = classNames(
|
||||||
'toolbar-dropdown',
|
'toolbar-dropdown',
|
||||||
|
this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption
|
||||||
|
? 'multi-option-style'
|
||||||
|
: 'foldout-style',
|
||||||
this.props.dropdownState,
|
this.props.dropdownState,
|
||||||
this.props.className
|
this.props.className
|
||||||
)
|
)
|
||||||
|
@ -383,16 +425,17 @@ export class ToolbarDropdown extends React.Component<
|
||||||
role={this.props.role}
|
role={this.props.role}
|
||||||
aria-expanded={ariaExpanded}
|
aria-expanded={ariaExpanded}
|
||||||
onDragOver={this.props.onDragOver}
|
onDragOver={this.props.onDragOver}
|
||||||
|
ref={this.rootDiv}
|
||||||
>
|
>
|
||||||
{this.renderDropdownContents()}
|
{this.renderDropdownContents()}
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
className={this.props.buttonClassName}
|
className={this.props.buttonClassName}
|
||||||
ref={this.onRef}
|
ref={this.innerButton}
|
||||||
icon={this.props.icon}
|
icon={this.props.icon}
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
description={this.props.description}
|
description={this.props.description}
|
||||||
tooltip={this.props.tooltip}
|
tooltip={this.props.tooltip}
|
||||||
onClick={this.onClick}
|
onClick={this.onMainButtonClick}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
onMouseEnter={this.props.onMouseEnter}
|
onMouseEnter={this.props.onMouseEnter}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
|
@ -407,8 +450,11 @@ export class ToolbarDropdown extends React.Component<
|
||||||
isOverflowed={this.props.isOverflowed}
|
isOverflowed={this.props.isOverflowed}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
{this.renderDropdownArrow()}
|
{this.props.dropdownStyle !== ToolbarDropdownStyle.MultiOption &&
|
||||||
|
this.renderDropdownArrow()}
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
|
{this.props.dropdownStyle === ToolbarDropdownStyle.MultiOption &&
|
||||||
|
this.renderDropdownArrow()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
123
app/src/ui/toolbar/push-pull-button-dropdown.tsx
Normal file
123
app/src/ui/toolbar/push-pull-button-dropdown.tsx
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Button } from '../lib/button'
|
||||||
|
import { Octicon, syncClockwise } from '../octicons'
|
||||||
|
import {
|
||||||
|
DropdownItem,
|
||||||
|
DropdownItemClassName,
|
||||||
|
DropdownItemType,
|
||||||
|
forcePushIcon,
|
||||||
|
} from './push-pull-button'
|
||||||
|
|
||||||
|
interface IPushPullButtonDropDownProps {
|
||||||
|
readonly itemTypes: ReadonlyArray<DropdownItemType>
|
||||||
|
/** The name of the remote. */
|
||||||
|
readonly remoteName: string | null
|
||||||
|
|
||||||
|
readonly fetch: () => void
|
||||||
|
readonly forcePushWithLease: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PushPullButtonDropDown extends React.Component<IPushPullButtonDropDownProps> {
|
||||||
|
private buttonsContainerRef: HTMLDivElement | null = null
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
window.addEventListener('keydown', this.onDropdownKeyDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
window.removeEventListener('keydown', this.onDropdownKeyDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onButtonsContainerRef = (ref: HTMLDivElement | null) => {
|
||||||
|
this.buttonsContainerRef = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDropdownKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// Allow using Up and Down arrow keys to navigate the dropdown items
|
||||||
|
// (equivalent to Tab and Shift+Tab)
|
||||||
|
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
const items = this.buttonsContainerRef?.querySelectorAll<HTMLElement>(
|
||||||
|
`.${DropdownItemClassName}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (items === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusedItem =
|
||||||
|
this.buttonsContainerRef?.querySelector<HTMLElement>(':focus')
|
||||||
|
if (!focusedItem) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusedIndex = Array.from(items).indexOf(focusedItem)
|
||||||
|
const nextIndex =
|
||||||
|
event.key === 'ArrowDown' ? focusedIndex + 1 : focusedIndex - 1
|
||||||
|
// http://javascript.about.com/od/problemsolving/a/modulobug.htm
|
||||||
|
const nextItem = items[(nextIndex + items.length) % items.length]
|
||||||
|
nextItem?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDropdownItemWithType(type: DropdownItemType): DropdownItem {
|
||||||
|
const { remoteName } = this.props
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case DropdownItemType.Fetch:
|
||||||
|
return {
|
||||||
|
title: `Fetch ${remoteName}`,
|
||||||
|
description: `Fetch the latest changes from ${remoteName}`,
|
||||||
|
action: this.props.fetch,
|
||||||
|
icon: syncClockwise,
|
||||||
|
}
|
||||||
|
case DropdownItemType.ForcePush:
|
||||||
|
return {
|
||||||
|
title: `Force push ${remoteName}`,
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Overwrite any changes on {remoteName} with your local changes
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<div className="warning">
|
||||||
|
<span className="warning-title">Warning:</span> A force push
|
||||||
|
will rewrite history on the remote. Any collaborators working on
|
||||||
|
this branch will need to reset their own local branch to match
|
||||||
|
the history of the remote.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
action: this.props.forcePushWithLease,
|
||||||
|
icon: forcePushIcon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderDropdownItem = (type: DropdownItemType) => {
|
||||||
|
const item = this.getDropdownItemWithType(type)
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={DropdownItemClassName}
|
||||||
|
key={type}
|
||||||
|
onClick={item.action}
|
||||||
|
>
|
||||||
|
<Octicon symbol={item.icon} />
|
||||||
|
<div className="text-container">
|
||||||
|
<div className="title">{item.title}</div>
|
||||||
|
<div className="detail">{item.description}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { itemTypes } = this.props
|
||||||
|
return (
|
||||||
|
<div className="push-pull-dropdown" ref={this.onButtonsContainerRef}>
|
||||||
|
{itemTypes.map(this.renderDropdownItem)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,18 @@ import { RelativeTime } from '../relative-time'
|
||||||
|
|
||||||
import { ToolbarButton, ToolbarButtonStyle } from './button'
|
import { ToolbarButton, ToolbarButtonStyle } from './button'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import {
|
||||||
|
DropdownState,
|
||||||
|
IToolbarDropdownProps,
|
||||||
|
ToolbarDropdown,
|
||||||
|
ToolbarDropdownStyle,
|
||||||
|
} from './dropdown'
|
||||||
|
import { FoldoutType } from '../../lib/app-state'
|
||||||
|
import { ForcePushBranchState } from '../../lib/rebase'
|
||||||
|
import { PushPullButtonDropDown } from './push-pull-button-dropdown'
|
||||||
|
import { enablePushPullFetchDropdown } from '../../lib/feature-flag'
|
||||||
|
|
||||||
|
export const DropdownItemClassName = 'push-pull-dropdown-item'
|
||||||
|
|
||||||
interface IPushPullButtonProps {
|
interface IPushPullButtonProps {
|
||||||
/**
|
/**
|
||||||
|
@ -52,8 +64,8 @@ interface IPushPullButtonProps {
|
||||||
/** Is the detached HEAD state related to a rebase or not? */
|
/** Is the detached HEAD state related to a rebase or not? */
|
||||||
readonly rebaseInProgress: boolean
|
readonly rebaseInProgress: boolean
|
||||||
|
|
||||||
/** If the current branch has been rebased, the user is permitted to force-push */
|
/** Force push state of the current branch */
|
||||||
readonly isForcePush: boolean
|
readonly forcePushBranchState: ForcePushBranchState
|
||||||
|
|
||||||
/** Whether this component should show its onboarding tutorial nudge arrow */
|
/** Whether this component should show its onboarding tutorial nudge arrow */
|
||||||
readonly shouldNudge: boolean
|
readonly shouldNudge: boolean
|
||||||
|
@ -62,6 +74,29 @@ interface IPushPullButtonProps {
|
||||||
* The number of tags that would get pushed if the user performed a push.
|
* The number of tags that would get pushed if the user performed a push.
|
||||||
*/
|
*/
|
||||||
readonly numTagsToPush: number
|
readonly numTagsToPush: number
|
||||||
|
|
||||||
|
/** Whether or not the push-pull dropdown is currently open */
|
||||||
|
readonly isDropdownOpen: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event handler for when the drop down is opened, or closed, by a pointer
|
||||||
|
* event or by pressing the space or enter key while focused.
|
||||||
|
*
|
||||||
|
* @param state - The new state of the drop down
|
||||||
|
*/
|
||||||
|
readonly onDropdownStateChanged: (state: DropdownState) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DropdownItemType {
|
||||||
|
Fetch = 'fetch',
|
||||||
|
ForcePush = 'force-push',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DropdownItem = {
|
||||||
|
readonly title: string
|
||||||
|
readonly description: string | JSX.Element
|
||||||
|
readonly action: () => void
|
||||||
|
readonly icon: OcticonSymbol.OcticonSymbolType
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAheadBehind(aheadBehind: IAheadBehind, numTagsToPush: number) {
|
function renderAheadBehind(aheadBehind: IAheadBehind, numTagsToPush: number) {
|
||||||
|
@ -104,165 +139,11 @@ function renderLastFetched(lastFetched: Date | null): JSX.Element | string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The common props for all button states */
|
|
||||||
const defaultProps = {
|
|
||||||
className: 'push-pull-button',
|
|
||||||
style: ToolbarButtonStyle.Subtitle,
|
|
||||||
}
|
|
||||||
|
|
||||||
function progressButton(progress: Progress, networkActionInProgress: boolean) {
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
{...defaultProps}
|
|
||||||
title={progress.title}
|
|
||||||
description={progress.description || 'Hang on…'}
|
|
||||||
progressValue={progress.value}
|
|
||||||
icon={syncClockwise}
|
|
||||||
iconClassName={networkActionInProgress ? 'spin' : ''}
|
|
||||||
tooltip={progress.description}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function publishRepositoryButton(onClick: () => void) {
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
{...defaultProps}
|
|
||||||
title="Publish repository"
|
|
||||||
description="Publish this repository to GitHub"
|
|
||||||
className="push-pull-button"
|
|
||||||
icon={OcticonSymbol.upload}
|
|
||||||
style={ToolbarButtonStyle.Subtitle}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function unbornRepositoryButton() {
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
{...defaultProps}
|
|
||||||
title="Publish branch"
|
|
||||||
description="Cannot publish unborn HEAD"
|
|
||||||
icon={OcticonSymbol.upload}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function detachedHeadButton(rebaseInProgress: boolean) {
|
|
||||||
const description = rebaseInProgress
|
|
||||||
? 'Rebase in progress'
|
|
||||||
: 'Cannot publish detached HEAD'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
{...defaultProps}
|
|
||||||
title="Publish branch"
|
|
||||||
description={description}
|
|
||||||
icon={OcticonSymbol.upload}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function publishBranchButton(
|
|
||||||
isGitHub: boolean,
|
|
||||||
onClick: () => void,
|
|
||||||
shouldNudge: boolean
|
|
||||||
) {
|
|
||||||
const description = isGitHub
|
|
||||||
? 'Publish this branch to GitHub'
|
|
||||||
: 'Publish this branch to the remote'
|
|
||||||
|
|
||||||
const className = classNames(defaultProps.className, 'nudge-arrow', {
|
|
||||||
'nudge-arrow-up': shouldNudge,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
{...defaultProps}
|
|
||||||
title="Publish branch"
|
|
||||||
description={description}
|
|
||||||
icon={OcticonSymbol.upload}
|
|
||||||
onClick={onClick}
|
|
||||||
className={className}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchButton(
|
|
||||||
remoteName: string,
|
|
||||||
aheadBehind: IAheadBehind,
|
|
||||||
numTagsToPush: number,
|
|
||||||
lastFetched: Date | null,
|
|
||||||
onClick: () => void
|
|
||||||
) {
|
|
||||||
const title = `Fetch ${remoteName}`
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
{...defaultProps}
|
|
||||||
title={title}
|
|
||||||
description={renderLastFetched(lastFetched)}
|
|
||||||
icon={syncClockwise}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
|
||||||
</ToolbarButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function pullButton(
|
|
||||||
remoteName: string,
|
|
||||||
aheadBehind: IAheadBehind,
|
|
||||||
numTagsToPush: number,
|
|
||||||
lastFetched: Date | null,
|
|
||||||
pullWithRebase: boolean,
|
|
||||||
onClick: () => void
|
|
||||||
) {
|
|
||||||
const title = pullWithRebase
|
|
||||||
? `Pull ${remoteName} with rebase`
|
|
||||||
: `Pull ${remoteName}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
{...defaultProps}
|
|
||||||
title={title}
|
|
||||||
description={renderLastFetched(lastFetched)}
|
|
||||||
icon={OcticonSymbol.arrowDown}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
|
||||||
</ToolbarButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushButton(
|
|
||||||
remoteName: string,
|
|
||||||
aheadBehind: IAheadBehind,
|
|
||||||
numTagsToPush: number,
|
|
||||||
lastFetched: Date | null,
|
|
||||||
onClick: () => void
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
{...defaultProps}
|
|
||||||
title={`Push ${remoteName}`}
|
|
||||||
description={renderLastFetched(lastFetched)}
|
|
||||||
icon={OcticonSymbol.arrowUp}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
|
||||||
</ToolbarButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This represents the "double arrow" icon used to show a force-push, and is a
|
* This represents the "double arrow" icon used to show a force-push, and is a
|
||||||
* less complicated icon than the generated Octicon from the `octicons` package.
|
* less complicated icon than the generated Octicon from the `octicons` package.
|
||||||
*/
|
*/
|
||||||
const forcePushIcon: OcticonSymbol.OcticonSymbolType = {
|
export const forcePushIcon: OcticonSymbol.OcticonSymbolType = {
|
||||||
w: 10,
|
w: 10,
|
||||||
h: 16,
|
h: 16,
|
||||||
d:
|
d:
|
||||||
|
@ -273,51 +154,80 @@ const forcePushIcon: OcticonSymbol.OcticonSymbolType = {
|
||||||
fr: 'evenodd',
|
fr: 'evenodd',
|
||||||
}
|
}
|
||||||
|
|
||||||
function forcePushButton(
|
|
||||||
remoteName: string,
|
|
||||||
aheadBehind: IAheadBehind,
|
|
||||||
numTagsToPush: number,
|
|
||||||
lastFetched: Date | null,
|
|
||||||
onClick: () => void
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
{...defaultProps}
|
|
||||||
title={`Force push ${remoteName}`}
|
|
||||||
description={renderLastFetched(lastFetched)}
|
|
||||||
icon={forcePushIcon}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
|
||||||
</ToolbarButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A button which pushes, pulls, or updates depending on the state of the
|
* A button which pushes, pulls, or updates depending on the state of the
|
||||||
* repository.
|
* repository.
|
||||||
*/
|
*/
|
||||||
export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
|
export class PushPullButton extends React.Component<IPushPullButtonProps> {
|
||||||
|
/** The common props for all button states */
|
||||||
|
private defaultButtonProps() {
|
||||||
|
return {
|
||||||
|
className: 'push-pull-button',
|
||||||
|
style: ToolbarButtonStyle.Subtitle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The common props for all dropdown states */
|
||||||
|
private defaultDropdownProps(): Omit<
|
||||||
|
IToolbarDropdownProps,
|
||||||
|
'dropdownContentRenderer'
|
||||||
|
> {
|
||||||
|
return {
|
||||||
|
buttonClassName: 'push-pull-button',
|
||||||
|
style: ToolbarButtonStyle.Subtitle,
|
||||||
|
dropdownStyle: ToolbarDropdownStyle.MultiOption,
|
||||||
|
dropdownState: this.props.isDropdownOpen ? 'open' : 'closed',
|
||||||
|
onDropdownStateChanged: this.props.onDropdownStateChanged,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeDropdown() {
|
||||||
|
this.props.dispatcher.closeFoldout(FoldoutType.PushPull)
|
||||||
|
}
|
||||||
|
|
||||||
private push = () => {
|
private push = () => {
|
||||||
|
this.closeDropdown()
|
||||||
this.props.dispatcher.push(this.props.repository)
|
this.props.dispatcher.push(this.props.repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
private forcePushWithLease = () => {
|
private forcePushWithLease = () => {
|
||||||
|
this.closeDropdown()
|
||||||
this.props.dispatcher.confirmOrForcePush(this.props.repository)
|
this.props.dispatcher.confirmOrForcePush(this.props.repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
private pull = () => {
|
private pull = () => {
|
||||||
|
this.closeDropdown()
|
||||||
this.props.dispatcher.pull(this.props.repository)
|
this.props.dispatcher.pull(this.props.repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetch = () => {
|
private fetch = () => {
|
||||||
|
this.closeDropdown()
|
||||||
this.props.dispatcher.fetch(
|
this.props.dispatcher.fetch(
|
||||||
this.props.repository,
|
this.props.repository,
|
||||||
FetchType.UserInitiatedTask
|
FetchType.UserInitiatedTask
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDropdownContentRenderer(
|
||||||
|
itemTypes: ReadonlyArray<DropdownItemType>
|
||||||
|
) {
|
||||||
|
return () => {
|
||||||
|
return (
|
||||||
|
<PushPullButtonDropDown
|
||||||
|
itemTypes={itemTypes}
|
||||||
|
remoteName={this.props.remoteName}
|
||||||
|
fetch={this.fetch}
|
||||||
|
forcePushWithLease={this.forcePushWithLease}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
return this.renderButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderButton() {
|
||||||
const {
|
const {
|
||||||
progress,
|
progress,
|
||||||
networkActionInProgress,
|
networkActionInProgress,
|
||||||
|
@ -329,28 +239,28 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
|
||||||
rebaseInProgress,
|
rebaseInProgress,
|
||||||
lastFetched,
|
lastFetched,
|
||||||
pullWithRebase,
|
pullWithRebase,
|
||||||
isForcePush,
|
forcePushBranchState,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
return progressButton(progress, networkActionInProgress)
|
return this.progressButton(progress, networkActionInProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteName === null) {
|
if (remoteName === null) {
|
||||||
return publishRepositoryButton(this.push)
|
return this.publishRepositoryButton(this.push)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tipState === TipState.Unborn) {
|
if (tipState === TipState.Unborn) {
|
||||||
return unbornRepositoryButton()
|
return this.unbornRepositoryButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tipState === TipState.Detached) {
|
if (tipState === TipState.Detached) {
|
||||||
return detachedHeadButton(rebaseInProgress)
|
return this.detachedHeadButton(rebaseInProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aheadBehind === null) {
|
if (aheadBehind === null) {
|
||||||
const isGitHubRepository = repository.gitHubRepository !== null
|
const isGitHubRepository = repository.gitHubRepository !== null
|
||||||
return publishBranchButton(
|
return this.publishBranchButton(
|
||||||
isGitHubRepository,
|
isGitHubRepository,
|
||||||
this.push,
|
this.push,
|
||||||
this.props.shouldNudge
|
this.props.shouldNudge
|
||||||
|
@ -360,17 +270,11 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
|
||||||
const { ahead, behind } = aheadBehind
|
const { ahead, behind } = aheadBehind
|
||||||
|
|
||||||
if (ahead === 0 && behind === 0 && numTagsToPush === 0) {
|
if (ahead === 0 && behind === 0 && numTagsToPush === 0) {
|
||||||
return fetchButton(
|
return this.fetchButton(remoteName, lastFetched, this.fetch)
|
||||||
remoteName,
|
|
||||||
aheadBehind,
|
|
||||||
numTagsToPush,
|
|
||||||
lastFetched,
|
|
||||||
this.fetch
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isForcePush) {
|
if (forcePushBranchState === ForcePushBranchState.Recommended) {
|
||||||
return forcePushButton(
|
return this.forcePushButton(
|
||||||
remoteName,
|
remoteName,
|
||||||
aheadBehind,
|
aheadBehind,
|
||||||
numTagsToPush,
|
numTagsToPush,
|
||||||
|
@ -380,17 +284,18 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (behind > 0) {
|
if (behind > 0) {
|
||||||
return pullButton(
|
return this.pullButton(
|
||||||
remoteName,
|
remoteName,
|
||||||
aheadBehind,
|
aheadBehind,
|
||||||
numTagsToPush,
|
numTagsToPush,
|
||||||
lastFetched,
|
lastFetched,
|
||||||
pullWithRebase || false,
|
pullWithRebase || false,
|
||||||
|
forcePushBranchState,
|
||||||
this.pull
|
this.pull
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pushButton(
|
return this.pushButton(
|
||||||
remoteName,
|
remoteName,
|
||||||
aheadBehind,
|
aheadBehind,
|
||||||
numTagsToPush,
|
numTagsToPush,
|
||||||
|
@ -398,4 +303,254 @@ export class PushPullButton extends React.Component<IPushPullButtonProps, {}> {
|
||||||
this.push
|
this.push
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private progressButton(progress: Progress, networkActionInProgress: boolean) {
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
{...this.defaultButtonProps()}
|
||||||
|
title={progress.title}
|
||||||
|
description={progress.description || 'Hang on…'}
|
||||||
|
progressValue={progress.value}
|
||||||
|
icon={syncClockwise}
|
||||||
|
iconClassName={networkActionInProgress ? 'spin' : ''}
|
||||||
|
tooltip={progress.description}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private publishRepositoryButton(onClick: () => void) {
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
{...this.defaultButtonProps()}
|
||||||
|
title="Publish repository"
|
||||||
|
description="Publish this repository to GitHub"
|
||||||
|
className="push-pull-button"
|
||||||
|
icon={OcticonSymbol.upload}
|
||||||
|
style={ToolbarButtonStyle.Subtitle}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private unbornRepositoryButton() {
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
{...this.defaultButtonProps()}
|
||||||
|
title="Publish branch"
|
||||||
|
description="Cannot publish unborn HEAD"
|
||||||
|
icon={OcticonSymbol.upload}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private detachedHeadButton(rebaseInProgress: boolean) {
|
||||||
|
const description = rebaseInProgress
|
||||||
|
? 'Rebase in progress'
|
||||||
|
: 'Cannot publish detached HEAD'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
{...this.defaultButtonProps()}
|
||||||
|
title="Publish branch"
|
||||||
|
description={description}
|
||||||
|
icon={OcticonSymbol.upload}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private publishBranchButton(
|
||||||
|
isGitHub: boolean,
|
||||||
|
onClick: () => void,
|
||||||
|
shouldNudge: boolean
|
||||||
|
) {
|
||||||
|
const description = isGitHub
|
||||||
|
? 'Publish this branch to GitHub'
|
||||||
|
: 'Publish this branch to the remote'
|
||||||
|
|
||||||
|
if (!enablePushPullFetchDropdown()) {
|
||||||
|
const className = classNames(
|
||||||
|
this.defaultButtonProps().className,
|
||||||
|
'nudge-arrow',
|
||||||
|
{
|
||||||
|
'nudge-arrow-up': shouldNudge,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
{...this.defaultButtonProps()}
|
||||||
|
title="Publish branch"
|
||||||
|
description={description}
|
||||||
|
icon={OcticonSymbol.upload}
|
||||||
|
onClick={onClick}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = classNames(
|
||||||
|
this.defaultDropdownProps().className,
|
||||||
|
'nudge-arrow',
|
||||||
|
{
|
||||||
|
'nudge-arrow-up': shouldNudge,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarDropdown
|
||||||
|
{...this.defaultDropdownProps()}
|
||||||
|
title="Publish branch"
|
||||||
|
description={description}
|
||||||
|
icon={OcticonSymbol.upload}
|
||||||
|
onClick={onClick}
|
||||||
|
className={className}
|
||||||
|
dropdownContentRenderer={this.getDropdownContentRenderer([
|
||||||
|
DropdownItemType.Fetch,
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchButton(
|
||||||
|
remoteName: string,
|
||||||
|
lastFetched: Date | null,
|
||||||
|
onClick: () => void
|
||||||
|
) {
|
||||||
|
const title = `Fetch ${remoteName}`
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
{...this.defaultButtonProps()}
|
||||||
|
title={title}
|
||||||
|
description={renderLastFetched(lastFetched)}
|
||||||
|
icon={syncClockwise}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private pullButton(
|
||||||
|
remoteName: string,
|
||||||
|
aheadBehind: IAheadBehind,
|
||||||
|
numTagsToPush: number,
|
||||||
|
lastFetched: Date | null,
|
||||||
|
pullWithRebase: boolean,
|
||||||
|
forcePushBranchState: ForcePushBranchState,
|
||||||
|
onClick: () => void
|
||||||
|
) {
|
||||||
|
const title = pullWithRebase
|
||||||
|
? `Pull ${remoteName} with rebase`
|
||||||
|
: `Pull ${remoteName}`
|
||||||
|
|
||||||
|
const dropdownItemTypes = [DropdownItemType.Fetch]
|
||||||
|
|
||||||
|
if (forcePushBranchState !== ForcePushBranchState.NotAvailable) {
|
||||||
|
dropdownItemTypes.push(DropdownItemType.ForcePush)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enablePushPullFetchDropdown()) {
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
{...this.defaultButtonProps()}
|
||||||
|
title={title}
|
||||||
|
description={renderLastFetched(lastFetched)}
|
||||||
|
icon={OcticonSymbol.arrowDown}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
||||||
|
</ToolbarButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarDropdown
|
||||||
|
{...this.defaultDropdownProps()}
|
||||||
|
title={title}
|
||||||
|
description={renderLastFetched(lastFetched)}
|
||||||
|
icon={OcticonSymbol.arrowDown}
|
||||||
|
onClick={onClick}
|
||||||
|
dropdownContentRenderer={this.getDropdownContentRenderer(
|
||||||
|
dropdownItemTypes
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
||||||
|
</ToolbarDropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private pushButton(
|
||||||
|
remoteName: string,
|
||||||
|
aheadBehind: IAheadBehind,
|
||||||
|
numTagsToPush: number,
|
||||||
|
lastFetched: Date | null,
|
||||||
|
onClick: () => void
|
||||||
|
) {
|
||||||
|
if (!enablePushPullFetchDropdown()) {
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
{...this.defaultButtonProps()}
|
||||||
|
title={`Push ${remoteName}`}
|
||||||
|
description={renderLastFetched(lastFetched)}
|
||||||
|
icon={OcticonSymbol.arrowUp}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
||||||
|
</ToolbarButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarDropdown
|
||||||
|
{...this.defaultDropdownProps()}
|
||||||
|
title={`Push ${remoteName}`}
|
||||||
|
description={renderLastFetched(lastFetched)}
|
||||||
|
icon={OcticonSymbol.arrowUp}
|
||||||
|
onClick={onClick}
|
||||||
|
dropdownContentRenderer={this.getDropdownContentRenderer([
|
||||||
|
DropdownItemType.Fetch,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
||||||
|
</ToolbarDropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private forcePushButton(
|
||||||
|
remoteName: string,
|
||||||
|
aheadBehind: IAheadBehind,
|
||||||
|
numTagsToPush: number,
|
||||||
|
lastFetched: Date | null,
|
||||||
|
onClick: () => void
|
||||||
|
) {
|
||||||
|
if (!enablePushPullFetchDropdown()) {
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
{...this.defaultButtonProps()}
|
||||||
|
title={`Force push ${remoteName}`}
|
||||||
|
description={renderLastFetched(lastFetched)}
|
||||||
|
icon={forcePushIcon}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
||||||
|
</ToolbarButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarDropdown
|
||||||
|
{...this.defaultDropdownProps()}
|
||||||
|
title={`Force push ${remoteName}`}
|
||||||
|
description={renderLastFetched(lastFetched)}
|
||||||
|
icon={forcePushIcon}
|
||||||
|
onClick={onClick}
|
||||||
|
dropdownContentRenderer={this.getDropdownContentRenderer([
|
||||||
|
DropdownItemType.Fetch,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{renderAheadBehind(aheadBehind, numTagsToPush)}
|
||||||
|
</ToolbarDropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
@import 'ui/toolbar/toolbar';
|
@import 'ui/toolbar/toolbar';
|
||||||
@import 'ui/toolbar/button';
|
@import 'ui/toolbar/button';
|
||||||
@import 'ui/toolbar/dropdown';
|
@import 'ui/toolbar/dropdown';
|
||||||
|
@import 'ui/toolbar/push-pull-button';
|
||||||
@import 'ui/tab-bar';
|
@import 'ui/tab-bar';
|
||||||
@import 'ui/panel';
|
@import 'ui/panel';
|
||||||
@import 'ui/popup';
|
@import 'ui/popup';
|
||||||
|
|
|
@ -248,6 +248,8 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
|
||||||
--toolbar-button-focus-progress-color: #{$gray-700};
|
--toolbar-button-focus-progress-color: #{$gray-700};
|
||||||
--toolbar-button-hover-progress-color: #{$gray-700};
|
--toolbar-button-hover-progress-color: #{$gray-700};
|
||||||
--toolbar-dropdown-open-progress-color: #{$gray-200};
|
--toolbar-dropdown-open-progress-color: #{$gray-200};
|
||||||
|
--toolbar-dropdown-text-warning-color: #{$yellow-800};
|
||||||
|
--toolbar-dropdown-text-hover-color: var(--box-hover-text-color);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App menu bar colors (Windows/Linux only)
|
* App menu bar colors (Windows/Linux only)
|
||||||
|
|
|
@ -174,6 +174,8 @@ body.theme-dark {
|
||||||
--toolbar-button-focus-progress-color: #{$gray-700};
|
--toolbar-button-focus-progress-color: #{$gray-700};
|
||||||
--toolbar-button-hover-progress-color: #{$gray-700};
|
--toolbar-button-hover-progress-color: #{$gray-700};
|
||||||
--toolbar-dropdown-open-progress-color: #{$gray-200};
|
--toolbar-dropdown-open-progress-color: #{$gray-200};
|
||||||
|
--toolbar-dropdown-text-warning-color: #{$yellow-700};
|
||||||
|
--toolbar-dropdown-text-hover-color: #{$white};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App menu bar colors (Windows/Linux only)
|
* App menu bar colors (Windows/Linux only)
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
.toolbar-button {
|
.toolbar-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
// Make sure the contents shrink beyond their intrinsic width
|
// Make sure the contents shrink beyond their intrinsic width
|
||||||
// See https://css-tricks.com/flexbox-truncated-text/
|
// See https://css-tricks.com/flexbox-truncated-text/
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
@ -11,6 +15,11 @@
|
||||||
// above all the other content.
|
// above all the other content.
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.toolbar-dropdown-button {
|
||||||
|
width: 40px;
|
||||||
|
height: 49px;
|
||||||
|
}
|
||||||
|
|
||||||
// General button behavior, mostly resets.
|
// General button behavior, mostly resets.
|
||||||
// For the button content styling see second button style. Note that we
|
// For the button content styling see second button style. Note that we
|
||||||
// explicitly use > here to only target the direct descendant button since
|
// explicitly use > here to only target the direct descendant button since
|
||||||
|
|
|
@ -3,13 +3,21 @@
|
||||||
// See https://css-tricks.com/flexbox-truncated-text/
|
// See https://css-tricks.com/flexbox-truncated-text/
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
& > .toolbar-button {
|
& > .toolbar-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .toolbar-dropdown-arrow-button {
|
||||||
|
width: 39px;
|
||||||
|
}
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
& > .toolbar-button > button {
|
&.foldout-style > .toolbar-button > button,
|
||||||
|
&.multi-option-style > .toolbar-dropdown-arrow-button > button {
|
||||||
color: var(--toolbar-button-active-color);
|
color: var(--toolbar-button-active-color);
|
||||||
background-color: var(--toolbar-button-active-background-color);
|
background-color: var(--toolbar-button-active-background-color);
|
||||||
|
|
||||||
|
|
66
app/styles/ui/toolbar/_push-pull-button.scss
Normal file
66
app/styles/ui/toolbar/_push-pull-button.scss
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
.push-pull-dropdown {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 1px;
|
||||||
|
z-index: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.push-pull-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: fit-content;
|
||||||
|
padding: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--box-background-color);
|
||||||
|
white-space: normal;
|
||||||
|
|
||||||
|
// Unset styles from Button component
|
||||||
|
text-align: unset;
|
||||||
|
border: unset;
|
||||||
|
border-radius: unset;
|
||||||
|
box-shadow: unset !important;
|
||||||
|
|
||||||
|
.octicon {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override background on focus to keep the default color
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--box-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--box-hover-background-color) !important;
|
||||||
|
color: var(--toolbar-dropdown-text-hover-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
// Enforce this bottom border style even in focused state
|
||||||
|
border-bottom: 1px solid var(--box-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 3px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
color: var(--text-secondary-color);
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: var(--toolbar-dropdown-text-warning-color);
|
||||||
|
|
||||||
|
.warning-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,16 +37,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-dropdown {
|
.toolbar-button {
|
||||||
&.branch-button {
|
&.branch-toolbar-button {
|
||||||
width: 230px;
|
width: 230px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-button {
|
|
||||||
&.revert-progress {
|
&.revert-progress {
|
||||||
width: 230px;
|
width: 230px;
|
||||||
}
|
}
|
||||||
|
&.toolbar-dropdown-arrow-button {
|
||||||
|
width: 39px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"[Improved] Close repository list after creating or adding repositories - #15508. Thanks @angusdev!",
|
"[Improved] Close repository list after creating or adding repositories - #15508. Thanks @angusdev!",
|
||||||
"[Improved] Always show an error message when an update fails - #15530"
|
"[Improved] Always show an error message when an update fails - #15530"
|
||||||
],
|
],
|
||||||
|
"3.1.4": ["[Improved] Upgrade embedded Git to 2.35.6"],
|
||||||
"3.1.4-beta1": [
|
"3.1.4-beta1": [
|
||||||
"[Added] Add support for JetBrains Toolbox and JetBrains Fleet editor for Windows - #12912. Thanks @tsvetilian-ty!",
|
"[Added] Add support for JetBrains Toolbox and JetBrains Fleet editor for Windows - #12912. Thanks @tsvetilian-ty!",
|
||||||
"[Added] Add support for Emacs editor for Linux - #15857. Thanks @zipperer!",
|
"[Added] Add support for Emacs editor for Linux - #15857. Thanks @zipperer!",
|
||||||
|
|
|
@ -252,6 +252,7 @@ These editors are currently supported:
|
||||||
- [JetBrains PhpStorm](https://www.jetbrains.com/phpstorm/)
|
- [JetBrains PhpStorm](https://www.jetbrains.com/phpstorm/)
|
||||||
- [JetBrains PyCharm](https://www.jetbrains.com/pycharm/)
|
- [JetBrains PyCharm](https://www.jetbrains.com/pycharm/)
|
||||||
- [JetBrains RubyMine](https://www.jetbrains.com/rubymine/)
|
- [JetBrains RubyMine](https://www.jetbrains.com/rubymine/)
|
||||||
|
- [JetBrains CLion](https://www.jetbrains.com/clion/)
|
||||||
- [RStudio](https://rstudio.com/)
|
- [RStudio](https://rstudio.com/)
|
||||||
- [TextMate](https://macromates.com)
|
- [TextMate](https://macromates.com)
|
||||||
- [Brackets](http://brackets.io/)
|
- [Brackets](http://brackets.io/)
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"eslint": "eslint --cache --rulesdir ./eslint-rules \"./eslint-rules/**/*.js\" \"./script/**/*.ts{,x}\" \"./app/{src,typings,test}/**/*.{j,t}s{,x}\" \"./changelog.json\"",
|
"eslint": "eslint --cache --rulesdir ./eslint-rules \"./eslint-rules/**/*.js\" \"./script/**/*.ts{,x}\" \"./app/{src,typings,test}/**/*.{j,t}s{,x}\" \"./changelog.json\"",
|
||||||
"eslint-check": "eslint --print-config .eslintrc.* | eslint-config-prettier-check",
|
"eslint-check": "eslint --print-config .eslintrc.* | eslint-config-prettier-check",
|
||||||
"publish": "ts-node -P script/tsconfig.json script/publish.ts",
|
"publish": "ts-node -P script/tsconfig.json script/publish.ts",
|
||||||
|
"validate-electron-version": "ts-node -P script/tsconfig.json script/validate-electron-version.ts",
|
||||||
"clean-slate": "rimraf out node_modules app/node_modules && yarn",
|
"clean-slate": "rimraf out node_modules app/node_modules && yarn",
|
||||||
"rebuild-hard:dev": "yarn clean-slate && yarn build:dev",
|
"rebuild-hard:dev": "yarn clean-slate && yarn build:dev",
|
||||||
"rebuild-hard:prod": "yarn clean-slate && yarn build:prod",
|
"rebuild-hard:prod": "yarn clean-slate && yarn build:prod",
|
||||||
|
|
|
@ -199,7 +199,7 @@ function packageApp() {
|
||||||
new RegExp('/\\.git($|/)'),
|
new RegExp('/\\.git($|/)'),
|
||||||
new RegExp('/node_modules/\\.bin($|/)'),
|
new RegExp('/node_modules/\\.bin($|/)'),
|
||||||
],
|
],
|
||||||
appCopyright: 'Copyright © 2017 GitHub, Inc.',
|
appCopyright: 'Copyright © 2023 GitHub, Inc.',
|
||||||
|
|
||||||
// macOS
|
// macOS
|
||||||
appBundleId: getBundleID(),
|
appBundleId: getBundleID(),
|
||||||
|
|
53
script/validate-electron-version.ts
Normal file
53
script/validate-electron-version.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/* eslint-disable no-sync */
|
||||||
|
/// <reference path="./globals.d.ts" />
|
||||||
|
|
||||||
|
import * as distInfo from './dist-info'
|
||||||
|
|
||||||
|
type ChannelToValidate = 'production' | 'beta'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object states the valid/expected Electron versions for each publishable
|
||||||
|
* channel of GitHub Desktop.
|
||||||
|
*
|
||||||
|
* The purpose of this is to ensure that we don't accidentally publish a
|
||||||
|
* production/beta/test build with the wrong version of Electron, which could
|
||||||
|
* cause really bad regressions to our users, and also the inability to go back
|
||||||
|
* to a previous version of GitHub Desktop without losing all settings.
|
||||||
|
*/
|
||||||
|
const ValidElectronVersions: Record<ChannelToValidate, string> = {
|
||||||
|
production: '19.0.0',
|
||||||
|
beta: '19.0.0',
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = getChannelToValidate()
|
||||||
|
|
||||||
|
if (channel === null) {
|
||||||
|
console.log(
|
||||||
|
`No need to validate the Electron version of a ${distInfo.getChannel()} build.`
|
||||||
|
)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedVersion = ValidElectronVersions[channel]
|
||||||
|
const pkg: Package = require('../package.json')
|
||||||
|
const actualVersion = pkg.devDependencies?.electron
|
||||||
|
|
||||||
|
if (actualVersion !== expectedVersion) {
|
||||||
|
console.error(
|
||||||
|
`The Electron version for the ${channel} channel is incorrect. Expected ${expectedVersion} but found ${actualVersion}.`
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`The Electron version for the ${channel} channel is correct: ${actualVersion}.`
|
||||||
|
)
|
||||||
|
|
||||||
|
function getChannelToValidate(): ChannelToValidate | null {
|
||||||
|
const channel = distInfo.getChannel()
|
||||||
|
return isChannelToValidate(channel) ? channel : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChannelToValidate(channel: string): channel is ChannelToValidate {
|
||||||
|
return Object.keys(ValidElectronVersions).includes(channel)
|
||||||
|
}
|
Loading…
Reference in a new issue