From f001beddfa654be6bb8c20616613e3fd2f2eca5e Mon Sep 17 00:00:00 2001 From: Ahnaf Mahmud <44692189+infinitepower18@users.noreply.github.com> Date: Tue, 6 Sep 2022 22:49:56 +0600 Subject: [PATCH 001/262] Add winget as community release --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b572740774..663a8dfedf 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,7 @@ The release notes for the latest beta versions are available [here](https://desk There are several community-supported package managers that can be used to install GitHub Desktop: - - Windows users can install using [Chocolatey](https://chocolatey.org/) package manager: - `c:\> choco install github-desktop` + - Windows users can install using [winget](https://docs.microsoft.com/en-us/windows/package-manager/winget/) `c:/> winget install github-desktop` or [Chocolatey](https://chocolatey.org/) `c:\> choco install github-desktop` - macOS users can install using [Homebrew](https://brew.sh/) package manager: `$ brew install --cask github` From a5d27abc450f5c39ba0fdba457a917b74c70a8fe Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:35:23 -0400 Subject: [PATCH 002/262] Create pull request diff component --- .../open-pull-request/pull-request-diff.tsx | 22 +++++++++++++++++++ app/styles/_ui.scss | 1 + app/styles/ui/_pull-request-diff.scss | 0 3 files changed, 23 insertions(+) create mode 100644 app/src/ui/open-pull-request/pull-request-diff.tsx create mode 100644 app/styles/ui/_pull-request-diff.scss diff --git a/app/src/ui/open-pull-request/pull-request-diff.tsx b/app/src/ui/open-pull-request/pull-request-diff.tsx new file mode 100644 index 0000000000..b2968cca89 --- /dev/null +++ b/app/src/ui/open-pull-request/pull-request-diff.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import { IDiff } from '../../models/diff' + +interface IPullRequestDiffProps { + readonly diff: IDiff | null +} + +/** + * A component for viewing the file diff for a pull request. + */ +export class PullRequestDiff extends React.Component< + IPullRequestDiffProps, + {} +> { + private renderDiff() { + return 'Diff' + } + + public render() { + return
{this.renderDiff()}
+ } +} diff --git a/app/styles/_ui.scss b/app/styles/_ui.scss index 7b65c88757..4b9da79b1c 100644 --- a/app/styles/_ui.scss +++ b/app/styles/_ui.scss @@ -100,3 +100,4 @@ @import 'ui/discard-changes-retry'; @import 'ui/_git-email-not-found-warning'; @import 'ui/_branch-select.scss'; +@import 'ui/_pull-request-diff'; diff --git a/app/styles/ui/_pull-request-diff.scss b/app/styles/ui/_pull-request-diff.scss new file mode 100644 index 0000000000..e69de29bb2 From b60a3d0d4e29a86c8b4c2418f6bee687edd6789b Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 20 Sep 2022 07:20:25 -0400 Subject: [PATCH 003/262] Use diff component --- .../ui/open-pull-request/open-pull-request-dialog.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 391ddbbf60..90efe5265b 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -5,6 +5,7 @@ import { Repository } from '../../models/repository' import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog' import { Dispatcher } from '../dispatcher' import { OpenPullRequestDialogHeader } from './open-pull-request-header' +import { PullRequestDiff } from './pull-request-diff' interface IOpenPullRequestDialogProps { readonly repository: Repository @@ -70,7 +71,13 @@ export class OpenPullRequestDialog extends React.ComponentContent + return
{this.renderFilesChanged()}
+ } + + private renderFilesChanged() { + const { commitSelection } = this.props.pullRequestState + const { diff } = commitSelection + return } private renderFooter() { From 4ccda73ce605ca8673e13390af2227d919f8a836 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 20 Sep 2022 08:03:49 -0400 Subject: [PATCH 004/262] Displaying the diff of the selected file --- app/src/ui/app.tsx | 10 ++- .../open-pull-request-dialog.tsx | 39 ++++++++-- .../open-pull-request/pull-request-diff.tsx | 78 ++++++++++++++++++- app/styles/ui/_pull-request-diff.scss | 4 + app/styles/ui/dialogs/_open-pull-request.scss | 5 ++ 5 files changed, 127 insertions(+), 9 deletions(-) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 7ae6ef43b2..a3cd3dc838 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2243,7 +2243,12 @@ export class App extends React.Component { ) } case PopupType.StartPullRequest: { - const { selectedState } = this.state + const { + selectedState, + imageDiffType, + hideWhitespaceInHistoryDiff, + showSideBySideDiff, + } = this.state if ( selectedState == null || selectedState.type !== SelectionType.Repository @@ -2270,9 +2275,12 @@ export class App extends React.Component { currentBranch={currentBranch} defaultBranch={defaultBranch} dispatcher={this.props.dispatcher} + hideWhitespaceInDiff={hideWhitespaceInHistoryDiff} + imageDiffType={imageDiffType} pullRequestState={pullRequestState} recentBranches={recentBranches} repository={repository} + showSideBySideDiff={showSideBySideDiff} onDismissed={onPopupDismissedFn} /> ) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 90efe5265b..79c9b24b7d 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { IPullRequestState } from '../../lib/app-state' import { Branch } from '../../models/branch' +import { ImageDiffType } from '../../models/diff' import { Repository } from '../../models/repository' import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog' import { Dispatcher } from '../dispatcher' @@ -36,6 +37,15 @@ interface IOpenPullRequestDialogProps { */ readonly recentBranches: ReadonlyArray + /** 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 + /** Called to dismiss the dialog */ readonly onDismissed: () => void } @@ -71,13 +81,31 @@ export class OpenPullRequestDialog extends React.Component{this.renderFilesChanged()} + return
{this.renderFilesChanged()}
} private renderFilesChanged() { - const { commitSelection } = this.props.pullRequestState - const { diff } = commitSelection - return + const { + dispatcher, + hideWhitespaceInDiff, + imageDiffType, + pullRequestState, + repository, + } = this.props + const { commitSelection } = pullRequestState + const { diff, file } = commitSelection + + return ( + + ) } private renderFooter() { @@ -100,8 +128,7 @@ export class OpenPullRequestDialog extends React.Component {this.renderHeader()} -
{this.renderContent()}
- + {this.renderContent()} {this.renderFooter()} ) diff --git a/app/src/ui/open-pull-request/pull-request-diff.tsx b/app/src/ui/open-pull-request/pull-request-diff.tsx index b2968cca89..9149303c6b 100644 --- a/app/src/ui/open-pull-request/pull-request-diff.tsx +++ b/app/src/ui/open-pull-request/pull-request-diff.tsx @@ -1,8 +1,29 @@ import * as React from 'react' -import { IDiff } from '../../models/diff' +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' interface IPullRequestDiffProps { + readonly repository: Repository + readonly dispatcher: Dispatcher + + /** The file whose diff should be displayed. */ + readonly selectedFile: CommittedFileChange | null + + /** 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 } /** @@ -12,11 +33,64 @@ export class PullRequestDiff extends React.Component< IPullRequestDiffProps, {} > { + /** + * 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.onHideWhitespaceInHistoryDiffChanged( + hideWhitespaceInDiff, + this.props.repository, + selectedFile as CommittedFileChange + ) + } + + /** + * 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 renderDiff() { - return 'Diff' + const { diff, selectedFile } = this.props + + if (diff === null || selectedFile === null) { + return + } + + const { + repository, + imageDiffType, + hideWhitespaceInDiff, + showSideBySideDiff, + } = this.props + + return ( + + ) } public render() { + // TODO: handle empty change set return
{this.renderDiff()}
} } diff --git a/app/styles/ui/_pull-request-diff.scss b/app/styles/ui/_pull-request-diff.scss index e69de29bb2..981ec1fdb1 100644 --- a/app/styles/ui/_pull-request-diff.scss +++ b/app/styles/ui/_pull-request-diff.scss @@ -0,0 +1,4 @@ +.pull-request-diff-viewer { + height: 500px; + display: flex; +} diff --git a/app/styles/ui/dialogs/_open-pull-request.scss b/app/styles/ui/dialogs/_open-pull-request.scss index b981ddd470..9738b2709c 100644 --- a/app/styles/ui/dialogs/_open-pull-request.scss +++ b/app/styles/ui/dialogs/_open-pull-request.scss @@ -15,4 +15,9 @@ padding: var(--spacing-half); } } + + .content { + height: 500px; + overflow: hidden; + } } From 2131d6af7d351c5fbf57d89ce236ac5bde839867 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 20 Sep 2022 08:58:30 -0400 Subject: [PATCH 005/262] Move needed properties to popup model --- app/src/lib/stores/app-store.ts | 29 +++++++++++++++++++++++++++++ app/src/models/popup.ts | 12 +++++++++++- app/src/ui/app.tsx | 27 +++++++-------------------- app/src/ui/dispatcher/dispatcher.ts | 4 ---- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index a3577dd0f2..750edbb3d7 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7194,6 +7194,35 @@ export class AppStore extends TypedBaseStore { changesetData.files[0] ) } + + const { pullRequestState } = this.repositoryStateCache.get(repository) + if (pullRequestState === null) { + // This shouldn't happen.. we just initialized it. + sendNonFatalException( + 'startPullRequest', + new Error( + 'Failed to start pull request because pull request state was null' + ) + ) + return + } + + const { allBranches, recentBranches } = branchesState + const { imageDiffType, hideWhitespaceInHistoryDiff, showSideBySideDiff } = + this.getState() + + this._showPopup({ + type: PopupType.StartPullRequest, + allBranches, + currentBranch, + defaultBranch, + hideWhitespaceInHistoryDiff, + imageDiffType, + pullRequestState, + recentBranches, + repository, + showSideBySideDiff, + }) } public async _changePullRequestFileSelection( diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index e8ab5268fa..76064bd3c7 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -14,7 +14,7 @@ import { Commit, CommitOneLine, ICommitContext } from './commit' import { IStashEntry } from './stash-entry' import { Account } from '../models/account' import { Progress } from './progress' -import { ITextDiff, DiffSelection } from './diff' +import { ITextDiff, DiffSelection, ImageDiffType } from './diff' import { RepositorySettingsTab } from '../ui/repository-settings/repository-settings' import { ICommitMessage } from './commit-message' import { IAuthor } from './author' @@ -22,6 +22,7 @@ import { IRefCheck } from '../lib/ci-checks/ci-checks' import { GitHubRepository } from './github-repository' import { ValidNotificationPullRequestReview } from '../lib/valid-notification-pull-request-review' import { UnreachableCommitsTab } from '../ui/history/unreachable-commits-dialog' +import { IPullRequestState } from '../lib/app-state' export enum PopupType { RenameBranch = 1, @@ -362,4 +363,13 @@ export type Popup = } | { type: PopupType.StartPullRequest + allBranches: ReadonlyArray + currentBranch: Branch + defaultBranch: Branch | null + hideWhitespaceInHistoryDiff: boolean + imageDiffType: ImageDiffType + pullRequestState: IPullRequestState + recentBranches: ReadonlyArray + repository: Repository + showSideBySideDiff: boolean } diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index a3cd3dc838..97533aec63 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2244,29 +2244,16 @@ export class App extends React.Component { } case PopupType.StartPullRequest: { const { - selectedState, + allBranches, + currentBranch, + defaultBranch, imageDiffType, hideWhitespaceInHistoryDiff, showSideBySideDiff, - } = this.state - if ( - selectedState == null || - selectedState.type !== SelectionType.Repository - ) { - return null - } - - const { state: repoState, repository } = selectedState - const { pullRequestState, branchesState } = repoState - if ( - pullRequestState === null || - branchesState.tip.kind !== TipState.Valid - ) { - return null - } - const { allBranches, recentBranches, defaultBranch, tip } = - branchesState - const currentBranch = tip.branch + pullRequestState, + recentBranches, + repository, + } = popup return ( Date: Tue, 20 Sep 2022 09:17:29 -0400 Subject: [PATCH 006/262] Rename to files-changed to reflect what all it will house --- app/src/ui/open-pull-request/open-pull-request-dialog.tsx | 4 ++-- ...pull-request-diff.tsx => pull-request-files-changed.tsx} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename app/src/ui/open-pull-request/{pull-request-diff.tsx => pull-request-files-changed.tsx} (95%) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 79c9b24b7d..410dc74e36 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -6,7 +6,7 @@ import { Repository } from '../../models/repository' import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog' import { Dispatcher } from '../dispatcher' import { OpenPullRequestDialogHeader } from './open-pull-request-header' -import { PullRequestDiff } from './pull-request-diff' +import { PullRequestFilesChanged } from './pull-request-files-changed' interface IOpenPullRequestDialogProps { readonly repository: Repository @@ -96,7 +96,7 @@ export class OpenPullRequestDialog extends React.Component { /** From e78ec961535cf5d7a94673eb23f0ee4cf0f4bd38 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 20 Sep 2022 09:55:57 -0400 Subject: [PATCH 007/262] Add non-functional file list --- .../open-pull-request-dialog.tsx | 4 +- .../pull-request-files-changed.tsx | 42 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 410dc74e36..62ed761daf 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -93,12 +93,14 @@ export class OpenPullRequestDialog extends React.Component + /** The diff that should be rendered */ readonly diff: IDiff | null @@ -89,8 +94,43 @@ export class PullRequestFilesChanged extends React.Component< ) } + private onDiffResize(newWidth: number) {} + + private onDiffSizeReset() {} + + private onContextMenu() {} + + private onFileSelected() {} + + private renderFileList() { + const { files, selectedFile } = this.props + + return ( + + + + ) + } + public render() { // TODO: handle empty change set - return
{this.renderDiff()}
+ return ( +
+ {this.renderFileList()} + {this.renderDiff()} +
+ ) } } From 44feaecde903603542bbc51f249be1a61b2b604b Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 20 Sep 2022 11:42:19 -0400 Subject: [PATCH 008/262] Add file selection in diff viewer --- app/src/lib/stores/app-store.ts | 14 +-------- app/src/models/popup.ts | 2 -- app/src/ui/app.tsx | 29 ++++++++++++++++++- app/src/ui/dispatcher/dispatcher.ts | 10 +++++++ .../pull-request-files-changed.tsx | 12 ++++++-- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 750edbb3d7..f4d491d612 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7195,18 +7195,6 @@ export class AppStore extends TypedBaseStore { ) } - const { pullRequestState } = this.repositoryStateCache.get(repository) - if (pullRequestState === null) { - // This shouldn't happen.. we just initialized it. - sendNonFatalException( - 'startPullRequest', - new Error( - 'Failed to start pull request because pull request state was null' - ) - ) - return - } - const { allBranches, recentBranches } = branchesState const { imageDiffType, hideWhitespaceInHistoryDiff, showSideBySideDiff } = this.getState() @@ -7218,7 +7206,6 @@ export class AppStore extends TypedBaseStore { defaultBranch, hideWhitespaceInHistoryDiff, imageDiffType, - pullRequestState, recentBranches, repository, showSideBySideDiff, @@ -7252,6 +7239,7 @@ export class AppStore extends TypedBaseStore { diff: null, }) ) + this.emitUpdate() if (commitSHAs.length === 0) { diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 76064bd3c7..08ac4139c6 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -22,7 +22,6 @@ import { IRefCheck } from '../lib/ci-checks/ci-checks' import { GitHubRepository } from './github-repository' import { ValidNotificationPullRequestReview } from '../lib/valid-notification-pull-request-review' import { UnreachableCommitsTab } from '../ui/history/unreachable-commits-dialog' -import { IPullRequestState } from '../lib/app-state' export enum PopupType { RenameBranch = 1, @@ -368,7 +367,6 @@ export type Popup = defaultBranch: Branch | null hideWhitespaceInHistoryDiff: boolean imageDiffType: ImageDiffType - pullRequestState: IPullRequestState recentBranches: ReadonlyArray repository: Repository showSideBySideDiff: boolean diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 97533aec63..1e7a666893 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -158,6 +158,7 @@ import { SSHUserPassword } from './ssh/ssh-user-password' import { showContextualMenu } from '../lib/menu-item' import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog' import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-dialog' +import { sendNonFatalException } from '../lib/helpers/non-fatal-exception' const MinuteInMilliseconds = 1000 * 60 const HourInMilliseconds = MinuteInMilliseconds * 60 @@ -2243,6 +2244,21 @@ export class App extends React.Component { ) } case PopupType.StartPullRequest: { + // Intentionally chose to get the current pull request state on + // rerender because state variables such as file selection change + // via the dispatcher. + const pullRequestState = this.getPullRequestState() + if (pullRequestState === null) { + // This shouldn't happen.. + sendNonFatalException( + 'FailedToStartPullRequest', + new Error( + 'Failed to start pull request because pull request state was null' + ) + ) + return null + } + const { allBranches, currentBranch, @@ -2250,7 +2266,6 @@ export class App extends React.Component { imageDiffType, hideWhitespaceInHistoryDiff, showSideBySideDiff, - pullRequestState, recentBranches, repository, } = popup @@ -2277,6 +2292,18 @@ export class App extends React.Component { } } + private getPullRequestState() { + const { selectedState } = this.state + if ( + selectedState == null || + selectedState.type !== SelectionType.Repository + ) { + return null + } + + return selectedState.state.pullRequestState + } + private getWarnForcePushDialogOnBegin( onBegin: () => void, onPopupDismissedFn: () => void diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index 06c18a2fbe..ad4bd590e5 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -3964,4 +3964,14 @@ export class Dispatcher { public startPullRequest(repository: Repository) { this.appStore._startPullRequest(repository) } + + /** + * Change the selected changed file of the current pull request state. + */ + public changePullRequestFileSelection( + repository: Repository, + file: CommittedFileChange + ): Promise { + return this.appStore._changePullRequestFileSelection(repository, file) + } } diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index e667cca2ad..22a68d54ea 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -65,13 +65,14 @@ export class PullRequestFilesChanged extends React.Component< } private renderDiff() { - const { diff, selectedFile } = this.props + const { selectedFile } = this.props - if (diff === null || selectedFile === null) { + if (selectedFile === null) { return } const { + diff, repository, imageDiffType, hideWhitespaceInDiff, @@ -100,7 +101,12 @@ export class PullRequestFilesChanged extends React.Component< private onContextMenu() {} - private onFileSelected() {} + private onFileSelected = (file: CommittedFileChange) => { + this.props.dispatcher.changePullRequestFileSelection( + this.props.repository, + file + ) + } private renderFileList() { const { files, selectedFile } = this.props From 99447820dbddbde46e05a93d0b268a550bafcace Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 20 Sep 2022 11:57:40 -0400 Subject: [PATCH 009/262] Add file context menu --- app/src/lib/stores/app-store.ts | 9 +- app/src/models/popup.ts | 1 + app/src/ui/app.tsx | 2 + .../open-pull-request-dialog.tsx | 5 ++ .../pull-request-files-changed.tsx | 85 ++++++++++++++++++- 5 files changed, 98 insertions(+), 4 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index f4d491d612..6a7632e15f 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7196,8 +7196,12 @@ export class AppStore extends TypedBaseStore { } const { allBranches, recentBranches } = branchesState - const { imageDiffType, hideWhitespaceInHistoryDiff, showSideBySideDiff } = - this.getState() + const { + imageDiffType, + hideWhitespaceInHistoryDiff, + showSideBySideDiff, + selectedExternalEditor, + } = this.getState() this._showPopup({ type: PopupType.StartPullRequest, @@ -7209,6 +7213,7 @@ export class AppStore extends TypedBaseStore { recentBranches, repository, showSideBySideDiff, + externalEditorLabel: selectedExternalEditor ?? undefined, }) } diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 08ac4139c6..96fd09c3d9 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -365,6 +365,7 @@ export type Popup = allBranches: ReadonlyArray currentBranch: Branch defaultBranch: Branch | null + externalEditorLabel?: string hideWhitespaceInHistoryDiff: boolean imageDiffType: ImageDiffType recentBranches: ReadonlyArray diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 1e7a666893..fe2b5f8384 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2265,6 +2265,7 @@ export class App extends React.Component { defaultBranch, imageDiffType, hideWhitespaceInHistoryDiff, + externalEditorLabel, showSideBySideDiff, recentBranches, repository, @@ -2282,6 +2283,7 @@ export class App extends React.Component { pullRequestState={pullRequestState} recentBranches={recentBranches} repository={repository} + externalEditorLabel={externalEditorLabel} showSideBySideDiff={showSideBySideDiff} onDismissed={onPopupDismissedFn} /> diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 62ed761daf..2f65eb4d30 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -46,6 +46,9 @@ interface IOpenPullRequestDialogProps { /** The type of image diff to display. */ readonly imageDiffType: ImageDiffType + /** Label for selected external editor */ + readonly externalEditorLabel?: string + /** Called to dismiss the dialog */ readonly onDismissed: () => void } @@ -87,6 +90,7 @@ export class OpenPullRequestDialog extends React.Component { + 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. @@ -99,7 +120,67 @@ export class PullRequestFilesChanged extends React.Component< private onDiffSizeReset() {} - private onContextMenu() {} + private onFileContextMenu = async ( + file: CommittedFileChange, + event: React.MouseEvent + ) => { + 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' }, + ] + + showContextualMenu(items) + } private onFileSelected = (file: CommittedFileChange) => { this.props.dispatcher.changePullRequestFileSelection( @@ -124,7 +205,7 @@ export class PullRequestFilesChanged extends React.Component< onSelectedFileChanged={this.onFileSelected} selectedFile={selectedFile} availableWidth={400} - onContextMenu={this.onContextMenu} + onContextMenu={this.onFileContextMenu} /> ) From 892c4aa2ab0ebbfe4ecf49da6bca70a9dd20e009 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 21 Sep 2022 10:56:02 -0400 Subject: [PATCH 010/262] Make file list resizable --- app/src/lib/app-state.ts | 3 + app/src/lib/stores/app-store.ts | 69 +++++++++++++++++++ app/src/ui/app.tsx | 3 + app/src/ui/dispatcher/dispatcher.ts | 14 ++++ .../open-pull-request-dialog.tsx | 7 +- .../pull-request-files-changed.tsx | 27 +++++--- app/styles/ui/dialogs/_open-pull-request.scss | 3 + 7 files changed, 116 insertions(+), 10 deletions(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index 1f0e0d42f7..35a697597b 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -170,6 +170,9 @@ export interface IAppState { /** The width of the files list in the stash view */ readonly stashedFilesWidth: IConstrainedValue + /** The width of the files list in the pull request files changed view */ + readonly pullRequestFilesListWidth: IConstrainedValue + /** * Used to highlight access keys throughout the app when the * Alt key is pressed. Only applicable on non-macOS platforms. diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 6a7632e15f..83583fbbc8 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -323,6 +323,9 @@ const commitSummaryWidthConfigKey: string = 'commit-summary-width' const defaultStashedFilesWidth: number = 250 const stashedFilesWidthConfigKey: string = 'stashed-files-width' +const defaultPullRequestFileListWidth: number = 250 +const pullRequestFileListConfigKey: string = 'stashed-files-width' + const askToMoveToApplicationsFolderDefault: boolean = true const confirmRepoRemovalDefault: boolean = true const confirmDiscardChangesDefault: boolean = true @@ -424,6 +427,7 @@ export class AppStore extends TypedBaseStore { private sidebarWidth = constrain(defaultSidebarWidth) private commitSummaryWidth = constrain(defaultCommitSummaryWidth) private stashedFilesWidth = constrain(defaultStashedFilesWidth) + private pullRequestFileListWidth = constrain(defaultPullRequestFileListWidth) private windowState: WindowState | null = null private windowZoomFactor: number = 1 @@ -901,6 +905,7 @@ export class AppStore extends TypedBaseStore { sidebarWidth: this.sidebarWidth, commitSummaryWidth: this.commitSummaryWidth, stashedFilesWidth: this.stashedFilesWidth, + pullRequestFilesListWidth: this.pullRequestFileListWidth, appMenuState: this.appMenu ? this.appMenu.openMenus : [], highlightAccessKeys: this.highlightAccessKeys, isUpdateAvailableBannerVisible: this.isUpdateAvailableBannerVisible, @@ -1951,8 +1956,13 @@ export class AppStore extends TypedBaseStore { this.stashedFilesWidth = constrain( getNumber(stashedFilesWidthConfigKey, defaultStashedFilesWidth) ) + this.pullRequestFileListWidth = constrain( + getNumber(pullRequestFileListConfigKey, defaultPullRequestFileListWidth) + ) this.updateResizableConstraints() + // TODO: Initiliaze here for now... maybe move to dialog mounting + this.updatePullRequestResizableConstraints() this.askToMoveToApplicationsFolderSetting = getBoolean( askToMoveToApplicationsFolderKey, @@ -2077,6 +2087,41 @@ export class AppStore extends TypedBaseStore { this.stashedFilesWidth = constrain(this.stashedFilesWidth, 100, filesMax) } + /** + * Calculate the constraints of the resizable pane in the pull request dialog + * whenever the window dimensions change. + */ + private updatePullRequestResizableConstraints() { + // TODO: Get width of PR dialog -> determine if we will have default width + // for pr dialog. The goal is for it expand to fill some percent of + // available window so it will change on window resize. We may have some max + // value and min value of where to derive a default is we cannot obtain the + // width for some reason (like initialization nad no pr dialog is open) + // Thoughts -> ß + // 1. Use dialog id to grab dialog if exists, else use default + // 2. Pass dialog width up when and call this contrainst on dialog mounting + // to initialize and subscribe to window resize inside dialog to be able + // to pass up dialog width on window resize. + + // Get the width of the dialog + const available = 850 + const dialogPadding = 20 + + // This is a pretty silly width for a diff but it will fit ~9 chars per line + // in unified mode after subtracting the width of the unified gutter and ~4 + // chars per side in split diff mode. No one would want to use it this way + // but it doesn't break the layout and it allows users to temporarily + // maximize the width of the file list to see long path names. + const diffPaneMinWidth = 150 + const filesListMax = available - dialogPadding - diffPaneMinWidth + + this.pullRequestFileListWidth = constrain( + this.pullRequestFileListWidth, + 100, + filesListMax + ) + } + private updateSelectedExternalEditor( selectedEditor: string | null ): Promise { @@ -7285,6 +7330,30 @@ export class AppStore extends TypedBaseStore { this.emitUpdate() } + + public _setPullRequestFileListWidth(width: number): Promise { + this.pullRequestFileListWidth = { + ...this.pullRequestFileListWidth, + value: width, + } + setNumber(pullRequestFileListConfigKey, width) + this.updatePullRequestResizableConstraints() + this.emitUpdate() + + return Promise.resolve() + } + + public _resetPullRequestFileListWidth(): Promise { + this.pullRequestFileListWidth = { + ...this.pullRequestFileListWidth, + value: defaultPullRequestFileListWidth, + } + localStorage.removeItem(pullRequestFileListConfigKey) + this.updatePullRequestResizableConstraints() + this.emitUpdate() + + return Promise.resolve() + } } /** diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index fe2b5f8384..2f0945610b 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2259,6 +2259,8 @@ export class App extends React.Component { return null } + const { pullRequestFilesListWidth } = this.state + const { allBranches, currentBranch, @@ -2278,6 +2280,7 @@ export class App extends React.Component { currentBranch={currentBranch} defaultBranch={defaultBranch} dispatcher={this.props.dispatcher} + fileListWidth={pullRequestFilesListWidth} hideWhitespaceInDiff={hideWhitespaceInHistoryDiff} imageDiffType={imageDiffType} pullRequestState={pullRequestState} diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index ad4bd590e5..d6ac77637e 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -3974,4 +3974,18 @@ export class Dispatcher { ): Promise { 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 { + return this.appStore._setPullRequestFileListWidth(width) + } + + /** + * Reset the width of the file list column in the pull request files changed + */ + public resetPullRequestFileListWidth(): Promise { + return this.appStore._resetPullRequestFileListWidth() + } } diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 2f65eb4d30..0505d9e3e4 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { IPullRequestState } from '../../lib/app-state' +import { IConstrainedValue, IPullRequestState } from '../../lib/app-state' import { Branch } from '../../models/branch' import { ImageDiffType } from '../../models/diff' import { Repository } from '../../models/repository' @@ -49,6 +49,9 @@ interface IOpenPullRequestDialogProps { /** Label for selected external editor */ readonly externalEditorLabel?: string + /** Width to use for the files list pane in the files changed view */ + readonly fileListWidth: IConstrainedValue + /** Called to dismiss the dialog */ readonly onDismissed: () => void } @@ -95,6 +98,7 @@ export class OpenPullRequestDialog extends React.Component { + this.props.dispatcher.setPullRequestFileListWidth(width) + } - private onDiffSizeReset() {} + private onFileListSizeReset = () => { + this.props.dispatcher.resetPullRequestFileListWidth() + } private onFileContextMenu = async ( file: CommittedFileChange, @@ -190,21 +199,21 @@ export class PullRequestFilesChanged extends React.Component< } private renderFileList() { - const { files, selectedFile } = this.props + const { files, selectedFile, fileListWidth } = this.props return ( diff --git a/app/styles/ui/dialogs/_open-pull-request.scss b/app/styles/ui/dialogs/_open-pull-request.scss index 9738b2709c..c53f0896a1 100644 --- a/app/styles/ui/dialogs/_open-pull-request.scss +++ b/app/styles/ui/dialogs/_open-pull-request.scss @@ -1,4 +1,7 @@ .open-pull-request { + width: 850px; + max-width: none; + header.dialog-header { padding-bottom: var(--spacing); From 01076b96c4f98eee10299b7fd3c9419a65115d5e Mon Sep 17 00:00:00 2001 From: Daniel Ciaglia Date: Fri, 23 Sep 2022 21:04:16 +0200 Subject: [PATCH 011/262] (fix) #15348 // VSCodium not recognised as external editor Fixes https://github.com/desktop/desktop/issues/15348 --- app/src/lib/editors/darwin.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/lib/editors/darwin.ts b/app/src/lib/editors/darwin.ts index a13813951b..4376efb1a2 100644 --- a/app/src/lib/editors/darwin.ts +++ b/app/src/lib/editors/darwin.ts @@ -45,7 +45,10 @@ const editors: IDarwinExternalEditor[] = [ }, { name: 'VSCodium', - bundleIdentifiers: ['com.visualstudio.code.oss'], + bundleIdentifiers: [ + 'com.visualstudio.code.oss', + 'com.vscodium', + ], }, { name: 'Sublime Text', From e63dba3cb1a6a625849e4acefa929e18df1d870f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:31:53 +0000 Subject: [PATCH 012/262] Bump peter-evans/create-pull-request from 4.1.1 to 4.1.2 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.1.1 to 4.1.2. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v4.1.1...v4.1.2) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index b359d10533..3b3811a498 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -37,7 +37,7 @@ jobs: private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} - name: Create Release Pull Request - uses: peter-evans/create-pull-request@v4.1.1 + uses: peter-evans/create-pull-request@v4.1.2 if: | startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test') with: From 20fa2895966fad3974f1010c070a5db292a2a362 Mon Sep 17 00:00:00 2001 From: Patrick Organ Date: Tue, 27 Sep 2022 21:05:31 -0400 Subject: [PATCH 013/262] add phpstorm, webstorm editors --- app/src/lib/editors/linux.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/lib/editors/linux.ts b/app/src/lib/editors/linux.ts index 84806820d7..087638fee1 100644 --- a/app/src/lib/editors/linux.ts +++ b/app/src/lib/editors/linux.ts @@ -62,6 +62,14 @@ const editors: ILinuxExternalEditor[] = [ name: 'Lite XL', paths: ['/usr/bin/lite-xl'], }, + { + name: 'Jetbrains PhpStorm', + paths: ['/snap/bin/phpstorm'], + }, + { + name: 'Jetbrains WebStorm', + paths: ['/snap/bin/webstorm'], + } ] async function getAvailablePath(paths: string[]): Promise { From 0fab6943cbf45c9feac525eb36fc674ac4089b96 Mon Sep 17 00:00:00 2001 From: Tsvetilian Yankov Date: Wed, 28 Sep 2022 17:33:50 +0300 Subject: [PATCH 014/262] Add toggle discard stash popup option to the store --- app/src/lib/app-state.ts | 3 +++ app/src/lib/stores/app-store.ts | 18 ++++++++++++++++++ app/src/ui/app.tsx | 1 + app/src/ui/dispatcher/dispatcher.ts | 4 ++++ app/src/ui/preferences/preferences.tsx | 14 ++++++++++++++ app/src/ui/preferences/prompts.tsx | 22 ++++++++++++++++++++++ 6 files changed, 62 insertions(+) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index 1f0e0d42f7..d7277a281f 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -194,6 +194,9 @@ export interface IAppState { /** Whether we should show a confirmation dialog */ readonly askForConfirmationOnDiscardChangesPermanently: boolean + /** Should the app propt the user to confirm a discard stash */ + readonly askForConfirmationOnDiscardStash: boolean + /** Should the app prompt the user to confirm a force push? */ readonly askForConfirmationOnForcePush: boolean diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 750edbb3d7..3bdf748db4 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -327,11 +327,13 @@ const askToMoveToApplicationsFolderDefault: boolean = true const confirmRepoRemovalDefault: boolean = true const confirmDiscardChangesDefault: boolean = true const confirmDiscardChangesPermanentlyDefault: boolean = true +const confirmDiscardStashDefault: boolean = true const askForConfirmationOnForcePushDefault = true const confirmUndoCommitDefault: boolean = true const askToMoveToApplicationsFolderKey: string = 'askToMoveToApplicationsFolder' const confirmRepoRemovalKey: string = 'confirmRepoRemoval' const confirmDiscardChangesKey: string = 'confirmDiscardChanges' +const confirmDiscardStashKey: string = 'confirmDiscardStash' const confirmDiscardChangesPermanentlyKey: string = 'confirmDiscardChangesPermanentlyKey' const confirmForcePushKey: string = 'confirmForcePush' @@ -437,6 +439,7 @@ export class AppStore extends TypedBaseStore { private confirmDiscardChanges: boolean = confirmDiscardChangesDefault private confirmDiscardChangesPermanently: boolean = confirmDiscardChangesPermanentlyDefault + private confirmDiscardStash: boolean = confirmDiscardStashDefault private askForConfirmationOnForcePush = askForConfirmationOnForcePushDefault private confirmUndoCommit: boolean = confirmUndoCommitDefault private imageDiffType: ImageDiffType = imageDiffTypeDefault @@ -913,6 +916,7 @@ export class AppStore extends TypedBaseStore { askForConfirmationOnDiscardChanges: this.confirmDiscardChanges, askForConfirmationOnDiscardChangesPermanently: this.confirmDiscardChangesPermanently, + askForConfirmationOnDiscardStash: this.confirmDiscardStash, askForConfirmationOnForcePush: this.askForConfirmationOnForcePush, askForConfirmationOnUndoCommit: this.confirmUndoCommit, uncommittedChangesStrategy: this.uncommittedChangesStrategy, @@ -1974,6 +1978,11 @@ export class AppStore extends TypedBaseStore { confirmDiscardChangesPermanentlyDefault ) + this.confirmDiscardStash = getBoolean( + confirmDiscardStashKey, + confirmDiscardStashDefault + ) + this.askForConfirmationOnForcePush = getBoolean( confirmForcePushKey, askForConfirmationOnForcePushDefault @@ -5193,6 +5202,15 @@ export class AppStore extends TypedBaseStore { return Promise.resolve() } + public _setConfirmDiscardStashSetting(value: boolean): Promise { + this.confirmDiscardStash = value + + setBoolean(confirmDiscardStashKey, value) + this.emitUpdate() + + return Promise.resolve() + } + public _setConfirmForcePushSetting(value: boolean): Promise { this.askForConfirmationOnForcePush = value setBoolean(confirmForcePushKey, value) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 97533aec63..4d16414fba 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -1481,6 +1481,7 @@ export class App extends React.Component { confirmDiscardChangesPermanently={ this.state.askForConfirmationOnDiscardChangesPermanently } + confirmDiscardStash={this.state.askForConfirmationOnDiscardStash} confirmForcePush={this.state.askForConfirmationOnForcePush} confirmUndoCommit={this.state.askForConfirmationOnUndoCommit} uncommittedChangesStrategy={this.state.uncommittedChangesStrategy} diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index 06c18a2fbe..f60f35abd1 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -2338,6 +2338,10 @@ export class Dispatcher { await this.appStore._loadStatus(repository) } + public setConfirmDiscardStashSetting(value: boolean) { + return this.appStore._setConfirmDiscardStashSetting(value) + } + public setConfirmForcePushSetting(value: boolean) { return this.appStore._setConfirmForcePushSetting(value) } diff --git a/app/src/ui/preferences/preferences.tsx b/app/src/ui/preferences/preferences.tsx index c66de88fc0..e4616e9474 100644 --- a/app/src/ui/preferences/preferences.tsx +++ b/app/src/ui/preferences/preferences.tsx @@ -54,6 +54,7 @@ interface IPreferencesProps { readonly confirmRepositoryRemoval: boolean readonly confirmDiscardChanges: boolean readonly confirmDiscardChangesPermanently: boolean + readonly confirmDiscardStash: boolean readonly confirmForcePush: boolean readonly confirmUndoCommit: boolean readonly uncommittedChangesStrategy: UncommittedChangesStrategy @@ -79,6 +80,7 @@ interface IPreferencesState { readonly confirmRepositoryRemoval: boolean readonly confirmDiscardChanges: boolean readonly confirmDiscardChangesPermanently: boolean + readonly confirmDiscardStash: boolean readonly confirmForcePush: boolean readonly confirmUndoCommit: boolean readonly uncommittedChangesStrategy: UncommittedChangesStrategy @@ -121,6 +123,7 @@ export class Preferences extends React.Component< confirmRepositoryRemoval: false, confirmDiscardChanges: false, confirmDiscardChangesPermanently: false, + confirmDiscardStash: false, confirmForcePush: false, confirmUndoCommit: false, uncommittedChangesStrategy: defaultUncommittedChangesStrategy, @@ -178,6 +181,7 @@ export class Preferences extends React.Component< confirmDiscardChanges: this.props.confirmDiscardChanges, confirmDiscardChangesPermanently: this.props.confirmDiscardChangesPermanently, + confirmDiscardStash: this.props.confirmDiscardStash, confirmForcePush: this.props.confirmForcePush, confirmUndoCommit: this.props.confirmUndoCommit, uncommittedChangesStrategy: this.props.uncommittedChangesStrategy, @@ -333,12 +337,14 @@ export class Preferences extends React.Component< confirmDiscardChangesPermanently={ this.state.confirmDiscardChangesPermanently } + confirmDiscardStash={this.state.confirmDiscardStash} confirmForcePush={this.state.confirmForcePush} confirmUndoCommit={this.state.confirmUndoCommit} onConfirmRepositoryRemovalChanged={ this.onConfirmRepositoryRemovalChanged } onConfirmDiscardChangesChanged={this.onConfirmDiscardChangesChanged} + onConfirmDiscardStashChanged={this.onConfirmDiscardStashChanged} onConfirmForcePushChanged={this.onConfirmForcePushChanged} onConfirmDiscardChangesPermanentlyChanged={ this.onConfirmDiscardChangesPermanentlyChanged @@ -410,6 +416,10 @@ export class Preferences extends React.Component< this.setState({ confirmDiscardChanges: value }) } + private onConfirmDiscardStashChanged = (value: boolean) => { + this.setState({ confirmDiscardStash: value }) + } + private onConfirmDiscardChangesPermanentlyChanged = (value: boolean) => { this.setState({ confirmDiscardChangesPermanently: value }) } @@ -562,6 +572,10 @@ export class Preferences extends React.Component< this.state.confirmForcePush ) + await this.props.dispatcher.setConfirmDiscardStashSetting( + this.state.confirmDiscardStash + ) + await this.props.dispatcher.setConfirmUndoCommitSetting( this.state.confirmUndoCommit ) diff --git a/app/src/ui/preferences/prompts.tsx b/app/src/ui/preferences/prompts.tsx index 30ee4d83fd..ef65ec2738 100644 --- a/app/src/ui/preferences/prompts.tsx +++ b/app/src/ui/preferences/prompts.tsx @@ -6,10 +6,12 @@ interface IPromptsPreferencesProps { readonly confirmRepositoryRemoval: boolean readonly confirmDiscardChanges: boolean readonly confirmDiscardChangesPermanently: boolean + readonly confirmDiscardStash: boolean readonly confirmForcePush: boolean readonly confirmUndoCommit: boolean readonly onConfirmDiscardChangesChanged: (checked: boolean) => void readonly onConfirmDiscardChangesPermanentlyChanged: (checked: boolean) => void + readonly onConfirmDiscardStashChanged: (checked: boolean) => void readonly onConfirmRepositoryRemovalChanged: (checked: boolean) => void readonly onConfirmForcePushChanged: (checked: boolean) => void readonly onConfirmUndoCommitChanged: (checked: boolean) => void @@ -19,6 +21,7 @@ interface IPromptsPreferencesState { readonly confirmRepositoryRemoval: boolean readonly confirmDiscardChanges: boolean readonly confirmDiscardChangesPermanently: boolean + readonly confirmDiscardStash: boolean readonly confirmForcePush: boolean readonly confirmUndoCommit: boolean } @@ -35,6 +38,7 @@ export class Prompts extends React.Component< confirmDiscardChanges: this.props.confirmDiscardChanges, confirmDiscardChangesPermanently: this.props.confirmDiscardChangesPermanently, + confirmDiscardStash: this.props.confirmDiscardStash, confirmForcePush: this.props.confirmForcePush, confirmUndoCommit: this.props.confirmUndoCommit, } @@ -58,6 +62,15 @@ export class Prompts extends React.Component< this.props.onConfirmDiscardChangesPermanentlyChanged(value) } + private onConfirmDiscardStashChanged = ( + event: React.FormEvent + ) => { + const value = event.currentTarget.checked + + this.setState({ confirmDiscardStash: value }) + this.props.onConfirmDiscardStashChanged(value) + } + private onConfirmForcePushChanged = ( event: React.FormEvent ) => { @@ -116,6 +129,15 @@ export class Prompts extends React.Component< } onChange={this.onConfirmDiscardChangesPermanentlyChanged} /> + Date: Wed, 28 Sep 2022 17:36:12 +0300 Subject: [PATCH 015/262] Handle discard stash button when popup disabled --- app/src/ui/app.tsx | 3 ++ app/src/ui/repository.tsx | 4 +++ app/src/ui/stashing/stash-diff-header.tsx | 43 ++++++++++++++++++----- app/src/ui/stashing/stash-diff-viewer.tsx | 6 ++++ 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 4d16414fba..6609225ced 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2951,6 +2951,9 @@ export class App extends React.Component { askForConfirmationOnDiscardChanges={ state.askForConfirmationOnDiscardChanges } + askForConfirmationOnDiscardStash={ + state.askForConfirmationOnDiscardStash + } accounts={state.accounts} externalEditorLabel={externalEditorLabel} resolvedExternalEditor={state.resolvedExternalEditor} diff --git a/app/src/ui/repository.tsx b/app/src/ui/repository.tsx index e307461278..85e6e97484 100644 --- a/app/src/ui/repository.tsx +++ b/app/src/ui/repository.tsx @@ -50,6 +50,7 @@ interface IRepositoryViewProps { readonly hideWhitespaceInHistoryDiff: boolean readonly showSideBySideDiff: boolean readonly askForConfirmationOnDiscardChanges: boolean + readonly askForConfirmationOnDiscardStash: boolean readonly focusCommitMessage: boolean readonly commitSpellcheckEnabled: boolean readonly accounts: ReadonlyArray @@ -355,6 +356,9 @@ export class RepositoryView extends React.Component< fileListWidth={this.props.stashedFilesWidth} repository={this.props.repository} dispatcher={this.props.dispatcher} + askForConfirmationOnDiscardStash={ + this.props.askForConfirmationOnDiscardStash + } isWorkingTreeClean={isWorkingTreeClean} showSideBySideDiff={this.props.showSideBySideDiff} onOpenBinaryFile={this.onOpenBinaryFile} diff --git a/app/src/ui/stashing/stash-diff-header.tsx b/app/src/ui/stashing/stash-diff-header.tsx index b631a559ba..c9b8bbbacd 100644 --- a/app/src/ui/stashing/stash-diff-header.tsx +++ b/app/src/ui/stashing/stash-diff-header.tsx @@ -11,11 +11,13 @@ interface IStashDiffHeaderProps { readonly stashEntry: IStashEntry readonly repository: Repository readonly dispatcher: Dispatcher + readonly askForConfirmationOnDiscardStash: boolean readonly isWorkingTreeClean: boolean } interface IStashDiffHeaderState { readonly isRestoring: boolean + readonly isDiscarding: boolean } /** @@ -31,12 +33,13 @@ export class StashDiffHeader extends React.Component< this.state = { isRestoring: false, + isDiscarding: false, } } public render() { const { isWorkingTreeClean } = this.props - const { isRestoring } = this.state + const { isRestoring, isDiscarding } = this.state return (
@@ -44,10 +47,12 @@ export class StashDiffHeader extends React.Component<
{this.renderExplanatoryText()} @@ -80,13 +85,33 @@ export class StashDiffHeader extends React.Component< ) } - private onDiscardClick = () => { - const { dispatcher, repository, stashEntry } = this.props - dispatcher.showPopup({ - type: PopupType.ConfirmDiscardStash, - stash: stashEntry, + private onDiscardClick = async () => { + const { + dispatcher, repository, - }) + stashEntry, + askForConfirmationOnDiscardStash, + } = this.props + + if (!askForConfirmationOnDiscardStash) { + this.setState({ + isDiscarding: true, + }) + + try { + await dispatcher.dropStash(repository, stashEntry) + } finally { + this.setState({ + isDiscarding: false, + }) + } + } else { + dispatcher.showPopup({ + type: PopupType.ConfirmDiscardStash, + stash: stashEntry, + repository, + }) + } } private onRestoreClick = async () => { diff --git a/app/src/ui/stashing/stash-diff-viewer.tsx b/app/src/ui/stashing/stash-diff-viewer.tsx index bc3b467556..fc9baecaba 100644 --- a/app/src/ui/stashing/stash-diff-viewer.tsx +++ b/app/src/ui/stashing/stash-diff-viewer.tsx @@ -27,6 +27,9 @@ interface IStashDiffViewerProps { readonly repository: Repository readonly dispatcher: Dispatcher + /** Should the app propt the user to confirm a discard stash */ + readonly askForConfirmationOnDiscardStash: boolean + /** Whether we should display side by side diffs. */ readonly showSideBySideDiff: boolean @@ -113,6 +116,9 @@ export class StashDiffViewer extends React.PureComponent repository={repository} dispatcher={dispatcher} isWorkingTreeClean={isWorkingTreeClean} + askForConfirmationOnDiscardStash={ + this.props.askForConfirmationOnDiscardStash + } />
Date: Thu, 29 Sep 2022 07:48:48 -0400 Subject: [PATCH 016/262] Needs it's own key doesn't it.. Co-authored-by: Sergio Padrino --- app/src/lib/stores/app-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 83583fbbc8..db44b7a2c0 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -324,7 +324,7 @@ const defaultStashedFilesWidth: number = 250 const stashedFilesWidthConfigKey: string = 'stashed-files-width' const defaultPullRequestFileListWidth: number = 250 -const pullRequestFileListConfigKey: string = 'stashed-files-width' +const pullRequestFileListConfigKey: string = 'pull-request-files-width' const askToMoveToApplicationsFolderDefault: boolean = true const confirmRepoRemovalDefault: boolean = true From a2914b67b4be2ef764a4e2736476a20e87eb5417 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 29 Sep 2022 08:28:06 -0400 Subject: [PATCH 017/262] Create method for commit url generation --- app/src/lib/commit-url.ts | 24 ++++++++++++++++++++++++ app/src/ui/app.tsx | 23 +++++++++-------------- 2 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 app/src/lib/commit-url.ts diff --git a/app/src/lib/commit-url.ts b/app/src/lib/commit-url.ts new file mode 100644 index 0000000000..8dd293c8be --- /dev/null +++ b/app/src/lib/commit-url.ts @@ -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}` +} diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 2f0945610b..fcfc29e92e 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import * as crypto from 'crypto' import { TransitionGroup, CSSTransition } from 'react-transition-group' import { IAppState, @@ -159,6 +158,7 @@ import { showContextualMenu } from '../lib/menu-item' import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog' import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-dialog' import { sendNonFatalException } from '../lib/helpers/non-fatal-exception' +import { createCommitURL } from '../lib/commit-url' const MinuteInMilliseconds = 1000 * 60 const HourInMilliseconds = MinuteInMilliseconds * 60 @@ -3076,22 +3076,17 @@ export class App extends React.Component { return } - const baseURL = repository.gitHubRepository.htmlURL + const commitURL = createCommitURL( + repository.gitHubRepository, + SHA, + filePath + ) - let fileSuffix = '' - if (filePath != null) { - const fileHash = crypto - .createHash('sha256') - .update(filePath) - .digest('hex') - fileSuffix = '#diff-' + fileHash + if (commitURL === null) { + return } - if (baseURL) { - this.props.dispatcher.openInBrowser( - `${baseURL}/commit/${SHA}${fileSuffix}` - ) - } + this.props.dispatcher.openInBrowser(commitURL) } private onBranchDeleted = (repository: Repository) => { From 01099472bfceb26688a6729554cc475111dedecd Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 29 Sep 2022 08:28:37 -0400 Subject: [PATCH 018/262] Add latest non local commit to PR popup model --- app/src/lib/stores/app-store.ts | 7 ++++++- app/src/models/popup.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index db44b7a2c0..2c30b2afd4 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7185,7 +7185,8 @@ export class AppStore extends TypedBaseStore { } public async _startPullRequest(repository: Repository) { - const { branchesState } = this.repositoryStateCache.get(repository) + const { branchesState, localCommitSHAs } = + this.repositoryStateCache.get(repository) const { defaultBranch, tip } = branchesState if (defaultBranch === null || tip.kind !== TipState.Valid) { @@ -7259,6 +7260,10 @@ export class AppStore extends TypedBaseStore { repository, showSideBySideDiff, externalEditorLabel: selectedExternalEditor ?? undefined, + nonLocalCommitSHA: + commitSHAs.length > 0 && !localCommitSHAs.includes(commitSHAs[0]) + ? commitSHAs[0] + : null, }) } diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 96fd09c3d9..188d8f0b0c 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -371,4 +371,5 @@ export type Popup = recentBranches: ReadonlyArray repository: Repository showSideBySideDiff: boolean + nonLocalCommitSHA: string | null } From 3b0436d4f9abd9af84c9c447105d605dcadbe5bf Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 29 Sep 2022 08:28:57 -0400 Subject: [PATCH 019/262] Pass nonLocalCommitSHA down --- app/src/ui/app.tsx | 2 ++ app/src/ui/open-pull-request/open-pull-request-dialog.tsx | 6 ++++++ app/src/ui/open-pull-request/pull-request-files-changed.tsx | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index fcfc29e92e..ad04787b84 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2268,6 +2268,7 @@ export class App extends React.Component { imageDiffType, hideWhitespaceInHistoryDiff, externalEditorLabel, + nonLocalCommitSHA, showSideBySideDiff, recentBranches, repository, @@ -2283,6 +2284,7 @@ export class App extends React.Component { fileListWidth={pullRequestFilesListWidth} hideWhitespaceInDiff={hideWhitespaceInHistoryDiff} imageDiffType={imageDiffType} + nonLocalCommitSHA={nonLocalCommitSHA} pullRequestState={pullRequestState} recentBranches={recentBranches} repository={repository} diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 0505d9e3e4..4333ce7c85 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -52,6 +52,10 @@ interface IOpenPullRequestDialogProps { /** 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 + /** Called to dismiss the dialog */ readonly onDismissed: () => void } @@ -99,6 +103,7 @@ export class OpenPullRequestDialog extends React.Component Date: Thu, 29 Sep 2022 08:29:29 -0400 Subject: [PATCH 020/262] Add menu item to open file on GitHub --- .../pull-request-files-changed.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index 50f3a8825b..d9b645862d 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -22,6 +22,8 @@ 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' interface IPullRequestFilesChangedProps { readonly repository: Repository @@ -133,6 +135,27 @@ export class PullRequestFilesChanged extends React.Component< 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 @@ -192,6 +215,17 @@ export class PullRequestFilesChanged extends React.Component< { 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) } From 4fd9550e3af02f2b35ed60ce6b55a3c569d93166 Mon Sep 17 00:00:00 2001 From: Patrick Organ Date: Thu, 29 Sep 2022 09:19:45 -0400 Subject: [PATCH 021/262] apply formatting --- app/src/lib/editors/linux.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/editors/linux.ts b/app/src/lib/editors/linux.ts index 087638fee1..de7953eb3d 100644 --- a/app/src/lib/editors/linux.ts +++ b/app/src/lib/editors/linux.ts @@ -69,7 +69,7 @@ const editors: ILinuxExternalEditor[] = [ { name: 'Jetbrains WebStorm', paths: ['/snap/bin/webstorm'], - } + }, ] async function getAvailablePath(paths: string[]): Promise { From b09726226c7cc2ecb89538116bbaf1de00e1ef60 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:55:29 -0400 Subject: [PATCH 022/262] Move render methods together --- .../pull-request-files-changed.tsx | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index d9b645862d..15e491b8b3 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -96,37 +96,6 @@ export class PullRequestFilesChanged extends React.Component< this.props.dispatcher.changeImageDiffType(imageDiffType) } - private renderDiff() { - const { selectedFile } = this.props - - if (selectedFile === null) { - return - } - - const { - diff, - repository, - imageDiffType, - hideWhitespaceInDiff, - showSideBySideDiff, - } = this.props - - return ( - - ) - } - private onFileListResize = (width: number) => { this.props.dispatcher.setPullRequestFileListWidth(width) } @@ -258,10 +227,42 @@ export class PullRequestFilesChanged extends React.Component< ) } + private renderDiff() { + const { selectedFile } = this.props + + if (selectedFile === null) { + return + } + + const { + diff, + repository, + imageDiffType, + hideWhitespaceInDiff, + showSideBySideDiff, + } = this.props + + return ( + + ) + } + public render() { // TODO: handle empty change set return (
+ {this.renderHeader()} {this.renderFileList()} {this.renderDiff()}
From 10fae06e5d4d163fd19c354e0919e0c98215cce2 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 21 Sep 2022 12:16:12 -0400 Subject: [PATCH 023/262] Add header and inset styling to files changed component --- .../pull-request-files-changed.tsx | 16 +++++++++++++--- app/styles/ui/_pull-request-diff.scss | 16 +++++++++++++--- app/styles/ui/dialogs/_open-pull-request.scss | 3 +-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index 15e491b8b3..d08e9a30a9 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -205,6 +205,14 @@ export class PullRequestFilesChanged extends React.Component< ) } + private renderHeader() { + return ( +
+
Showing changes from all commits
+
+ ) + } + private renderFileList() { const { files, selectedFile, fileListWidth } = this.props @@ -261,10 +269,12 @@ export class PullRequestFilesChanged extends React.Component< public render() { // TODO: handle empty change set return ( -
+
{this.renderHeader()} - {this.renderFileList()} - {this.renderDiff()} +
+ {this.renderFileList()} + {this.renderDiff()} +
) } diff --git a/app/styles/ui/_pull-request-diff.scss b/app/styles/ui/_pull-request-diff.scss index 981ec1fdb1..0c6ace3a3b 100644 --- a/app/styles/ui/_pull-request-diff.scss +++ b/app/styles/ui/_pull-request-diff.scss @@ -1,4 +1,14 @@ -.pull-request-diff-viewer { - height: 500px; - display: flex; +.pull-request-files-changed { + border: var(--base-border); + border-radius: var(--border-radius); + + .files-changed-header { + padding: var(--spacing-half); + border-bottom: var(--base-border); + } + + .files-diff-viewer { + height: 500px; + display: flex; + } } diff --git a/app/styles/ui/dialogs/_open-pull-request.scss b/app/styles/ui/dialogs/_open-pull-request.scss index c53f0896a1..0c007acb04 100644 --- a/app/styles/ui/dialogs/_open-pull-request.scss +++ b/app/styles/ui/dialogs/_open-pull-request.scss @@ -20,7 +20,6 @@ } .content { - height: 500px; - overflow: hidden; + padding: var(--spacing); } } From 15132f8840829a7b8295707b33370e8529861390 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 21 Sep 2022 12:26:15 -0400 Subject: [PATCH 024/262] Using regular spacing --- app/styles/ui/_pull-request-diff.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/ui/_pull-request-diff.scss b/app/styles/ui/_pull-request-diff.scss index 0c6ace3a3b..69954b1f55 100644 --- a/app/styles/ui/_pull-request-diff.scss +++ b/app/styles/ui/_pull-request-diff.scss @@ -3,7 +3,7 @@ border-radius: var(--border-radius); .files-changed-header { - padding: var(--spacing-half); + padding: var(--spacing); border-bottom: var(--base-border); } From 05a305e274eb08169523ae5b70f0e174644c5292 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Fri, 23 Sep 2022 10:04:34 -0400 Subject: [PATCH 025/262] Scope content class (so it doesn't interfere with split diffs) --- app/src/ui/open-pull-request/open-pull-request-dialog.tsx | 6 +++++- app/styles/ui/dialogs/_open-pull-request.scss | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 4333ce7c85..c6991954b0 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -91,7 +91,11 @@ export class OpenPullRequestDialog extends React.Component{this.renderFilesChanged()}
+ return ( +
+ {this.renderFilesChanged()} +
+ ) } private renderFilesChanged() { diff --git a/app/styles/ui/dialogs/_open-pull-request.scss b/app/styles/ui/dialogs/_open-pull-request.scss index 0c007acb04..2c6f174137 100644 --- a/app/styles/ui/dialogs/_open-pull-request.scss +++ b/app/styles/ui/dialogs/_open-pull-request.scss @@ -19,7 +19,7 @@ } } - .content { + .open-pull-request-content { padding: var(--spacing); } } From e33172adf6202647a9701798480c454784b2fd5a Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 22 Sep 2022 10:51:35 -0400 Subject: [PATCH 026/262] create reusable popover dropdown component based on branch select --- app/src/ui/lib/popover-dropdown.tsx | 125 +++++++++++++++++++++++++++ app/styles/_ui.scss | 3 +- app/styles/ui/_popover-dropdown.scss | 46 ++++++++++ 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 app/src/ui/lib/popover-dropdown.tsx create mode 100644 app/styles/ui/_popover-dropdown.scss diff --git a/app/src/ui/lib/popover-dropdown.tsx b/app/src/ui/lib/popover-dropdown.tsx new file mode 100644 index 0000000000..557676541a --- /dev/null +++ b/app/src/ui/lib/popover-dropdown.tsx @@ -0,0 +1,125 @@ +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' + +const defaultPopoverContentHeight = 300 +const maxPopoverContentHeight = 500 + +interface IPopoverDropdownProps { + readonly contentTitle: string + readonly buttonContent: JSX.Element | string +} + +interface IPopoverDropdownState { + readonly showPopover: boolean + readonly popoverContentHeight: number +} + +/** + * A select element for filter and selecting a branch. + */ +export class PopoverSelect 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 }) + } + + private closePopover = () => { + this.setState({ showPopover: false }) + } + + public renderPopover() { + if (!this.state.showPopover) { + return + } + + const { contentTitle } = this.props + const { popoverContentHeight } = this.state + const contentStyle = { height: `${popoverContentHeight}px` } + + return ( + +
+ {contentTitle} + +
+
+ {this.props.children} +
+
+ ) + } + + public render() { + return ( +
+ + {this.renderPopover()} +
+ ) + } +} diff --git a/app/styles/_ui.scss b/app/styles/_ui.scss index 4b9da79b1c..99b8c5aeb0 100644 --- a/app/styles/_ui.scss +++ b/app/styles/_ui.scss @@ -99,5 +99,6 @@ @import 'ui/_pull-request-quick-view'; @import 'ui/discard-changes-retry'; @import 'ui/_git-email-not-found-warning'; -@import 'ui/_branch-select.scss'; +@import 'ui/_branch-select'; @import 'ui/_pull-request-diff'; +@import 'ui/_popover-dropdown'; diff --git a/app/styles/ui/_popover-dropdown.scss b/app/styles/ui/_popover-dropdown.scss new file mode 100644 index 0000000000..a7999447c0 --- /dev/null +++ b/app/styles/ui/_popover-dropdown.scss @@ -0,0 +1,46 @@ +.popover-dropdown-component { + display: inline-flex; + + button { + border: none; + background-color: inherit; + border: none; + padding: 0; + margin: 0; + color: #149ad4; + font-weight: bold; + + &.button-component { + overflow: visible; + + &:hover, + &:focus { + border: none; + box-shadow: none; + } + } + } + + .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; + } + } +} From 7dcb83c656f4e6d2050e3548825b24f22009e890 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 22 Sep 2022 11:44:13 -0400 Subject: [PATCH 027/262] Make branch select component use the popover dropdown --- app/src/ui/branches/branch-select.tsx | 110 +++----------------------- app/src/ui/lib/popover-dropdown.tsx | 18 +++-- app/styles/ui/_branch-select.scss | 59 +------------- app/styles/ui/_popover-dropdown.scss | 3 +- 4 files changed, 28 insertions(+), 162 deletions(-) diff --git a/app/src/ui/branches/branch-select.tsx b/app/src/ui/branches/branch-select.tsx index a3cd692189..4cde775e46 100644 --- a/app/src/ui/branches/branch-select.tsx +++ b/app/src/ui/branches/branch-select.tsx @@ -1,19 +1,12 @@ import * as React from 'react' import { IMatches } from '../../lib/fuzzy-find' import { Branch } from '../../models/branch' -import { Button } from '../lib/button' import { ClickSource } from '../lib/list' -import { Popover } from '../lib/popover' -import { Ref } from '../lib/ref' -import { Octicon } from '../octicons' -import * as OcticonSymbol from '../octicons/octicons.generated' +import { PopoverDropdown } from '../lib/popover-dropdown' import { BranchList } from './branch-list' import { renderDefaultBranch } from './branch-renderer' import { IBranchListItem } from './group-branches' -const defaultDropdownListHeight = 300 -const maxDropdownListHeight = 500 - interface IBranchSelectProps { /** The initially selected branch. */ readonly branch: Branch @@ -43,10 +36,8 @@ interface IBranchSelectProps { } interface IBranchSelectState { - readonly showBranchDropdown: boolean readonly selectedBranch: Branch | null readonly filterText: string - readonly dropdownListHeight: number } /** @@ -56,67 +47,25 @@ export class BranchSelect extends React.Component< IBranchSelectProps, IBranchSelectState > { - private invokeButtonRef: HTMLButtonElement | null = null + private popoverRef = React.createRef() public constructor(props: IBranchSelectProps) { super(props) this.state = { - showBranchDropdown: false, selectedBranch: props.branch, filterText: '', - dropdownListHeight: defaultDropdownListHeight, } } - public componentDidMount() { - this.calculateDropdownListHeight() - } - - public componentDidUpdate() { - this.calculateDropdownListHeight() - } - - private calculateDropdownListHeight = () => { - if (this.invokeButtonRef === null) { - return - } - - const windowHeight = window.innerHeight - const bottomOfButton = this.invokeButtonRef.getBoundingClientRect().bottom - const listHeaderHeight = 75 - const calcMaxHeight = Math.round( - windowHeight - bottomOfButton - listHeaderHeight - ) - - const dropdownListHeight = - calcMaxHeight > maxDropdownListHeight - ? maxDropdownListHeight - : calcMaxHeight - if (dropdownListHeight !== this.state.dropdownListHeight) { - this.setState({ dropdownListHeight }) - } - } - - private onInvokeButtonRef = (buttonRef: HTMLButtonElement | null) => { - this.invokeButtonRef = buttonRef - } - - private toggleBranchDropdown = () => { - this.setState({ showBranchDropdown: !this.state.showBranchDropdown }) - } - - private closeBranchDropdown = () => { - this.setState({ showBranchDropdown: false }) - } - private renderBranch = (item: IBranchListItem, matches: IMatches) => { return renderDefaultBranch(item, matches, this.props.currentBranch) } private onItemClick = (branch: Branch, source: ClickSource) => { source.event.preventDefault() - this.setState({ showBranchDropdown: false, selectedBranch: branch }) + this.popoverRef.current?.closePopover() + this.setState({ selectedBranch: branch }) this.props.onChange?.(branch) } @@ -124,34 +73,19 @@ export class BranchSelect extends React.Component< this.setState({ filterText }) } - public renderBranchDropdown() { - if (!this.state.showBranchDropdown) { - return - } - + public render() { const { currentBranch, defaultBranch, recentBranches, allBranches } = this.props - const { filterText, selectedBranch, dropdownListHeight } = this.state + const { filterText, selectedBranch } = this.state return ( - -
- Choose a base branch - -
-
+ base: + -
-
- ) - } - - public render() { - return ( -
- - {this.renderBranchDropdown()} +
) } diff --git a/app/src/ui/lib/popover-dropdown.tsx b/app/src/ui/lib/popover-dropdown.tsx index 557676541a..d2e18518d1 100644 --- a/app/src/ui/lib/popover-dropdown.tsx +++ b/app/src/ui/lib/popover-dropdown.tsx @@ -3,11 +3,13 @@ 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 } @@ -18,9 +20,10 @@ interface IPopoverDropdownState { } /** - * A select element for filter and selecting a branch. + * A dropdown component for displaying a dropdown button that opens + * a popover to display contents relative to the button content. */ -export class PopoverSelect extends React.Component< +export class PopoverDropdown extends React.Component< IPopoverDropdownProps, IPopoverDropdownState > { @@ -72,11 +75,11 @@ export class PopoverSelect extends React.Component< this.setState({ showPopover: !this.state.showPopover }) } - private closePopover = () => { + public closePopover = () => { this.setState({ showPopover: false }) } - public renderPopover() { + private renderPopover() { if (!this.state.showPopover) { return } @@ -109,13 +112,16 @@ export class PopoverSelect extends React.Component< } public render() { + const { className, buttonContent } = this.props + const cn = classNames('popover-dropdown-component', className) + return ( -
+
{this.renderPopover()} diff --git a/app/styles/ui/_branch-select.scss b/app/styles/ui/_branch-select.scss index 38c9a62fc8..c0ae93fd27 100644 --- a/app/styles/ui/_branch-select.scss +++ b/app/styles/ui/_branch-select.scss @@ -1,66 +1,9 @@ .branch-select-component { - display: inline-flex; + display: inline; .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; - } - } } diff --git a/app/styles/ui/_popover-dropdown.scss b/app/styles/ui/_popover-dropdown.scss index a7999447c0..178674cb77 100644 --- a/app/styles/ui/_popover-dropdown.scss +++ b/app/styles/ui/_popover-dropdown.scss @@ -7,8 +7,9 @@ border: none; padding: 0; margin: 0; - color: #149ad4; + color: var(--link-button-color); font-weight: bold; + text-decoration: underline; &.button-component { overflow: visible; From c59263b92ba75b4ad5362f6493b9331839e40389 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 22 Sep 2022 12:50:23 -0400 Subject: [PATCH 028/262] Add diff options component --- app/src/ui/changes/changed-file-details.tsx | 3 +-- app/src/ui/diff/diff-options.tsx | 5 ++--- app/src/ui/history/commit-summary.tsx | 3 +-- .../pull-request-files-changed.tsx | 19 +++++++++++++++++++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/src/ui/changes/changed-file-details.tsx b/app/src/ui/changes/changed-file-details.tsx index b47b7d2918..cab1ce5d52 100644 --- a/app/src/ui/changes/changed-file-details.tsx +++ b/app/src/ui/changes/changed-file-details.tsx @@ -6,7 +6,6 @@ import { Octicon, iconForStatus } from '../octicons' import * as OcticonSymbol from '../octicons/octicons.generated' import { mapStatus } from '../../lib/status' import { DiffOptions } from '../diff/diff-options' -import { RepositorySectionTab } from '../../lib/app-state' interface IChangedFileDetailsProps { readonly path: string @@ -61,7 +60,7 @@ export class ChangedFileDetails extends React.Component< return ( - {this.props.sourceTab === RepositorySectionTab.Changes && ( + {this.props.isInteractiveDiff && (

Interacting with individual lines or hunks will be disabled while hiding whitespace. diff --git a/app/src/ui/history/commit-summary.tsx b/app/src/ui/history/commit-summary.tsx index 2182f63011..af82a4156e 100644 --- a/app/src/ui/history/commit-summary.tsx +++ b/app/src/ui/history/commit-summary.tsx @@ -12,7 +12,6 @@ import { CommitAttribution } from '../lib/commit-attribution' import { Tokenizer, TokenResult } from '../../lib/text-token-parser' import { wrapRichTextCommitMessage } from '../../lib/wrap-rich-text-commit-message' import { DiffOptions } from '../diff/diff-options' -import { RepositorySectionTab } from '../../lib/app-state' import { IChangesetData } from '../../lib/git' import { TooltippedContent } from '../lib/tooltipped-content' import { AppFileStatusKind } from '../../models/status' @@ -505,7 +504,7 @@ export class CommitSummary extends React.Component< title="Diff Options" > { + this.props.dispatcher.onShowSideBySideDiffChanged(showSideBySideDiff) + } + + private onDiffOptionsOpened = () => { + this.props.dispatcher.recordDiffOptionsViewed() + } + /** * Called when the user is viewing an image diff and requests * to change the diff presentation mode. @@ -209,6 +218,16 @@ export class PullRequestFilesChanged extends React.Component< return (

Showing changes from all commits
+ + +
) } From a0e9ea9ec2492366d1a82ad2db3f64cedccd4b2f Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 22 Sep 2022 12:54:47 -0400 Subject: [PATCH 029/262] Display settings to right Also rename style file name to match component name --- .../pull-request-files-changed.tsx | 22 +++++++++---------- app/styles/_ui.scss | 2 +- ....scss => _pull-request-files-changed.scss} | 5 +++++ 3 files changed, 17 insertions(+), 12 deletions(-) rename app/styles/ui/{_pull-request-diff.scss => _pull-request-files-changed.scss} (78%) diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index f2b4af91de..868ac1044f 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -217,17 +217,17 @@ export class PullRequestFilesChanged extends React.Component< private renderHeader() { return (
-
Showing changes from all commits
- - - +
+ Showing changes from all commits +
+
) } diff --git a/app/styles/_ui.scss b/app/styles/_ui.scss index 99b8c5aeb0..53c78ec973 100644 --- a/app/styles/_ui.scss +++ b/app/styles/_ui.scss @@ -100,5 +100,5 @@ @import 'ui/discard-changes-retry'; @import 'ui/_git-email-not-found-warning'; @import 'ui/_branch-select'; -@import 'ui/_pull-request-diff'; @import 'ui/_popover-dropdown'; +@import 'ui/_pull-request-files-changed'; diff --git a/app/styles/ui/_pull-request-diff.scss b/app/styles/ui/_pull-request-files-changed.scss similarity index 78% rename from app/styles/ui/_pull-request-diff.scss rename to app/styles/ui/_pull-request-files-changed.scss index 69954b1f55..ef3555c387 100644 --- a/app/styles/ui/_pull-request-diff.scss +++ b/app/styles/ui/_pull-request-files-changed.scss @@ -5,6 +5,11 @@ .files-changed-header { padding: var(--spacing); border-bottom: var(--base-border); + display: flex; + + .commits-displayed { + flex-grow: 1; + } } .files-diff-viewer { From 82b1c682ab8fbc82b4dcde8ac74470af7d981bd7 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 22 Sep 2022 12:56:59 -0400 Subject: [PATCH 030/262] Tidying --- app/src/ui/open-pull-request/pull-request-files-changed.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index 868ac1044f..6a8921e261 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -215,6 +215,7 @@ export class PullRequestFilesChanged extends React.Component< } private renderHeader() { + const { hideWhitespaceInDiff, showSideBySideDiff } = this.props return (
@@ -222,9 +223,9 @@ export class PullRequestFilesChanged extends React.Component<
From 147120897b306d47863e3e6ea4a8568fa2be6b15 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 22 Sep 2022 13:25:34 -0400 Subject: [PATCH 031/262] Get the diff state variables off the current state --- app/src/ui/app.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index ad04787b84..4a6e1a1caa 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2259,17 +2259,19 @@ export class App extends React.Component { return null } - const { pullRequestFilesListWidth } = this.state + const { + pullRequestFilesListWidth, + hideWhitespaceInHistoryDiff, + showSideBySideDiff, + } = this.state const { allBranches, currentBranch, defaultBranch, imageDiffType, - hideWhitespaceInHistoryDiff, externalEditorLabel, nonLocalCommitSHA, - showSideBySideDiff, recentBranches, repository, } = popup From e3165804529c33f526d957f03d35fcfd9101f1da Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 22 Sep 2022 13:54:17 -0400 Subject: [PATCH 032/262] Remove state tracked variables from popover --- app/src/lib/stores/app-store.ts | 9 +-------- app/src/models/popup.ts | 2 -- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 2c30b2afd4..7ad2314c38 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7242,23 +7242,16 @@ export class AppStore extends TypedBaseStore { } const { allBranches, recentBranches } = branchesState - const { - imageDiffType, - hideWhitespaceInHistoryDiff, - showSideBySideDiff, - selectedExternalEditor, - } = this.getState() + const { imageDiffType, selectedExternalEditor } = this.getState() this._showPopup({ type: PopupType.StartPullRequest, allBranches, currentBranch, defaultBranch, - hideWhitespaceInHistoryDiff, imageDiffType, recentBranches, repository, - showSideBySideDiff, externalEditorLabel: selectedExternalEditor ?? undefined, nonLocalCommitSHA: commitSHAs.length > 0 && !localCommitSHAs.includes(commitSHAs[0]) diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 188d8f0b0c..dfca4a6a28 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -366,10 +366,8 @@ export type Popup = currentBranch: Branch defaultBranch: Branch | null externalEditorLabel?: string - hideWhitespaceInHistoryDiff: boolean imageDiffType: ImageDiffType recentBranches: ReadonlyArray repository: Repository - showSideBySideDiff: boolean nonLocalCommitSHA: string | null } From 5be997fa708a1d0f5b107ab931cedc45333af245 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 22 Sep 2022 14:10:41 -0400 Subject: [PATCH 033/262] Add hideWhitespaceinPullRequestDiff tracking --- app/src/lib/app-state.ts | 3 +++ app/src/lib/stores/app-store.ts | 24 ++++++++++++++++++- app/src/ui/app.tsx | 4 ++-- app/src/ui/diff/diff-options.tsx | 2 +- app/src/ui/dispatcher/dispatcher.ts | 13 ++++++++++ .../pull-request-files-changed.tsx | 4 ++-- 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index 35a697597b..6b99e1ef19 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -233,6 +233,9 @@ export interface IAppState { /** Whether we should hide white space changes in history diff */ readonly hideWhitespaceInHistoryDiff: boolean + /** Whether we should hide white space changes in the pull request diff */ + readonly hideWhitespaceInPullRequestDiff: boolean + /** Whether we should show side by side diffs */ readonly showSideBySideDiff: boolean diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 7ad2314c38..970bcd9490 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -351,6 +351,8 @@ const hideWhitespaceInChangesDiffDefault = false const hideWhitespaceInChangesDiffKey = 'hide-whitespace-in-changes-diff' const hideWhitespaceInHistoryDiffDefault = false const hideWhitespaceInHistoryDiffKey = 'hide-whitespace-in-diff' +const hideWhitespaceInPullRequestDiffDefault = false +const hideWhitespaceInPullRequestDiffKey = 'hide-whitespace-in-diff' const commitSpellcheckEnabledDefault = true const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled' @@ -448,6 +450,8 @@ export class AppStore extends TypedBaseStore { hideWhitespaceInChangesDiffDefault private hideWhitespaceInHistoryDiff: boolean = hideWhitespaceInHistoryDiffDefault + private hideWhitespaceInPullRequestDiff: boolean = + hideWhitespaceInPullRequestDiffDefault /** Whether or not the spellchecker is enabled for commit summary and description */ private commitSpellcheckEnabled: boolean = commitSpellcheckEnabledDefault private showSideBySideDiff: boolean = ShowSideBySideDiffDefault @@ -925,6 +929,7 @@ export class AppStore extends TypedBaseStore { imageDiffType: this.imageDiffType, hideWhitespaceInChangesDiff: this.hideWhitespaceInChangesDiff, hideWhitespaceInHistoryDiff: this.hideWhitespaceInHistoryDiff, + hideWhitespaceInPullRequestDiff: this.hideWhitespaceInPullRequestDiff, showSideBySideDiff: this.showSideBySideDiff, selectedShell: this.selectedShell, repositoryFilterText: this.repositoryFilterText, @@ -2021,6 +2026,10 @@ export class AppStore extends TypedBaseStore { hideWhitespaceInHistoryDiffKey, false ) + this.hideWhitespaceInPullRequestDiff = getBoolean( + hideWhitespaceInPullRequestDiffKey, + false + ) this.commitSpellcheckEnabled = getBoolean( commitSpellcheckEnabledKey, commitSpellcheckEnabledDefault @@ -5324,6 +5333,19 @@ export class AppStore extends TypedBaseStore { } } + public _setHideWhitespaceInPullRequestDiff( + hideWhitespaceInDiff: boolean, + repository: Repository, + file: CommittedFileChange | null + ) { + setBoolean(hideWhitespaceInPullRequestDiffKey, hideWhitespaceInDiff) + this.hideWhitespaceInPullRequestDiff = hideWhitespaceInDiff + + if (file !== null) { + this._changePullRequestFileSelection(repository, file) + } + } + public _setShowSideBySideDiff(showSideBySideDiff: boolean) { if (showSideBySideDiff !== this.showSideBySideDiff) { setShowSideBySideDiff(showSideBySideDiff) @@ -7305,7 +7327,7 @@ export class AppStore extends TypedBaseStore { file, baseBranch.name, currentBranch.name, - this.hideWhitespaceInHistoryDiff, + this.hideWhitespaceInPullRequestDiff, commitSHAs[0] ) )) ?? null diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 4a6e1a1caa..8bb82c098a 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2261,7 +2261,7 @@ export class App extends React.Component { const { pullRequestFilesListWidth, - hideWhitespaceInHistoryDiff, + hideWhitespaceInPullRequestDiff, showSideBySideDiff, } = this.state @@ -2284,7 +2284,7 @@ export class App extends React.Component { defaultBranch={defaultBranch} dispatcher={this.props.dispatcher} fileListWidth={pullRequestFilesListWidth} - hideWhitespaceInDiff={hideWhitespaceInHistoryDiff} + hideWhitespaceInDiff={hideWhitespaceInPullRequestDiff} imageDiffType={imageDiffType} nonLocalCommitSHA={nonLocalCommitSHA} pullRequestState={pullRequestState} diff --git a/app/src/ui/diff/diff-options.tsx b/app/src/ui/diff/diff-options.tsx index f2ec38a893..f5791f5e44 100644 --- a/app/src/ui/diff/diff-options.tsx +++ b/app/src/ui/diff/diff-options.tsx @@ -10,7 +10,7 @@ interface IDiffOptionsProps { readonly hideWhitespaceChanges: boolean readonly onHideWhitespaceChangesChanged: ( hideWhitespaceChanges: boolean - ) => Promise + ) => Promise | void readonly showSideBySideDiff: boolean readonly onShowSideBySideDiffChanged: (showSideBySideDiff: boolean) => void diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index d6ac77637e..dec5515def 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -2122,6 +2122,19 @@ export class Dispatcher { ) } + /** Change the hide whitespace in pull request diff setting */ + public onHideWhitespaceInPullRequestDiffChanged( + hideWhitespaceInDiff: boolean, + repository: Repository, + file: CommittedFileChange | null = null + ) { + this.appStore._setHideWhitespaceInPullRequestDiff( + hideWhitespaceInDiff, + repository, + file + ) + } + /** Change the side by side diff setting */ public onShowSideBySideDiffChanged(showSideBySideDiff: boolean) { return this.appStore._setShowSideBySideDiff(showSideBySideDiff) diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index 6a8921e261..d297240562 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -60,7 +60,7 @@ interface IPullRequestFilesChangedProps { } /** - * A component for viewing the file diff for a pull request. + * A component for viewing the file changes for a pull request. */ export class PullRequestFilesChanged extends React.Component< IPullRequestFilesChangedProps, @@ -82,7 +82,7 @@ export class PullRequestFilesChanged extends React.Component< /** Called when the user changes the hide whitespace in diffs setting. */ private onHideWhitespaceInDiffChanged = (hideWhitespaceInDiff: boolean) => { const { selectedFile } = this.props - return this.props.dispatcher.onHideWhitespaceInHistoryDiffChanged( + return this.props.dispatcher.onHideWhitespaceInPullRequestDiffChanged( hideWhitespaceInDiff, this.props.repository, selectedFile as CommittedFileChange From 785664c7e91b57d4f044ea1d9202500f6301297d Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 28 Sep 2022 08:03:04 -0400 Subject: [PATCH 034/262] Remove copy/pasta casting --- app/src/ui/open-pull-request/pull-request-files-changed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index d297240562..a41d6c9d31 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -85,7 +85,7 @@ export class PullRequestFilesChanged extends React.Component< return this.props.dispatcher.onHideWhitespaceInPullRequestDiffChanged( hideWhitespaceInDiff, this.props.repository, - selectedFile as CommittedFileChange + selectedFile ) } From 27caadcde931c1fd584c53c92f7668a351a55821 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 28 Sep 2022 08:16:16 -0400 Subject: [PATCH 035/262] Move sideBySidediff state management to files changed component --- app/src/lib/stores/app-store.ts | 4 ++- app/src/models/popup.ts | 1 + app/src/ui/app.tsx | 8 +++--- .../pull-request-files-changed.tsx | 27 ++++++++++++------- .../ui/_pull-request-files-changed.scss | 4 +++ 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 970bcd9490..9674327244 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7264,7 +7264,8 @@ export class AppStore extends TypedBaseStore { } const { allBranches, recentBranches } = branchesState - const { imageDiffType, selectedExternalEditor } = this.getState() + const { imageDiffType, selectedExternalEditor, showSideBySideDiff } = + this.getState() this._showPopup({ type: PopupType.StartPullRequest, @@ -7279,6 +7280,7 @@ export class AppStore extends TypedBaseStore { commitSHAs.length > 0 && !localCommitSHAs.includes(commitSHAs[0]) ? commitSHAs[0] : null, + showSideBySideDiff, }) } diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index dfca4a6a28..57cc87c866 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -370,4 +370,5 @@ export type Popup = recentBranches: ReadonlyArray repository: Repository nonLocalCommitSHA: string | null + showSideBySideDiff: boolean } diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 8bb82c098a..d6cd421e39 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2259,11 +2259,8 @@ export class App extends React.Component { return null } - const { - pullRequestFilesListWidth, - hideWhitespaceInPullRequestDiff, - showSideBySideDiff, - } = this.state + const { pullRequestFilesListWidth, hideWhitespaceInPullRequestDiff } = + this.state const { allBranches, @@ -2274,6 +2271,7 @@ export class App extends React.Component { nonLocalCommitSHA, recentBranches, repository, + showSideBySideDiff, } = popup return ( diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index a41d6c9d31..e8ca091104 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -59,13 +59,23 @@ interface IPullRequestFilesChangedProps { 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) @@ -90,7 +100,7 @@ export class PullRequestFilesChanged extends React.Component< } private onShowSideBySideDiffChanged = (showSideBySideDiff: boolean) => { - this.props.dispatcher.onShowSideBySideDiffChanged(showSideBySideDiff) + this.setState({ showSideBySideDiff }) } private onDiffOptionsOpened = () => { @@ -215,7 +225,8 @@ export class PullRequestFilesChanged extends React.Component< } private renderHeader() { - const { hideWhitespaceInDiff, showSideBySideDiff } = this.props + const { hideWhitespaceInDiff } = this.props + const { showSideBySideDiff } = this.state return (
@@ -262,13 +273,9 @@ export class PullRequestFilesChanged extends React.Component< return } - const { - diff, - repository, - imageDiffType, - hideWhitespaceInDiff, - showSideBySideDiff, - } = this.props + const { diff, repository, imageDiffType, hideWhitespaceInDiff } = this.props + + const { showSideBySideDiff } = this.state return ( Date: Mon, 26 Sep 2022 13:01:55 -0400 Subject: [PATCH 036/262] Use MacOs/Windows Sentence casing --- app/src/ui/open-pull-request/open-pull-request-dialog.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index c6991954b0..f55546f465 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -134,8 +134,9 @@ export class OpenPullRequestDialog extends React.Component From f6a72527cc6cc8f550aa96bb6355a5d23b812b8c Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 26 Sep 2022 13:02:16 -0400 Subject: [PATCH 037/262] Add Enterprise label --- .../ui/open-pull-request/open-pull-request-dialog.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index f55546f465..4627d438f7 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { IConstrainedValue, IPullRequestState } from '../../lib/app-state' +import { getDotComAPIEndpoint } from '../../lib/api' import { Branch } from '../../models/branch' import { ImageDiffType } from '../../models/diff' import { Repository } from '../../models/repository' @@ -131,12 +132,20 @@ export class OpenPullRequestDialog extends React.Component From cad3813389608fd11b53cbc2e13268fc94f2a57e Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 28 Sep 2022 08:41:50 -0400 Subject: [PATCH 038/262] Add no changes div --- .../open-pull-request-dialog.tsx | 17 ++++++++++++++++- .../pull-request-files-changed.tsx | 1 - 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index c6991954b0..fe449d15a9 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -93,6 +93,7 @@ export class OpenPullRequestDialog extends React.Component + {this.renderNoChanges()} {this.renderFilesChanged()}
) @@ -110,9 +111,13 @@ export class OpenPullRequestDialog extends React.Component No changes!
+ } + private renderFooter() { return ( diff --git a/app/src/ui/open-pull-request/pull-request-files-changed.tsx b/app/src/ui/open-pull-request/pull-request-files-changed.tsx index d08e9a30a9..02d02e48be 100644 --- a/app/src/ui/open-pull-request/pull-request-files-changed.tsx +++ b/app/src/ui/open-pull-request/pull-request-files-changed.tsx @@ -267,7 +267,6 @@ export class PullRequestFilesChanged extends React.Component< } public render() { - // TODO: handle empty change set return (
{this.renderHeader()} From cec67105a1849417f8e788edb09717cdb02effe6 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 28 Sep 2022 08:41:56 -0400 Subject: [PATCH 039/262] Center no changes content --- app/styles/ui/dialogs/_open-pull-request.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/styles/ui/dialogs/_open-pull-request.scss b/app/styles/ui/dialogs/_open-pull-request.scss index 2c6f174137..6cdbb91e7f 100644 --- a/app/styles/ui/dialogs/_open-pull-request.scss +++ b/app/styles/ui/dialogs/_open-pull-request.scss @@ -22,4 +22,13 @@ .open-pull-request-content { padding: var(--spacing); } + + .open-pull-request-no-changes { + height: 100%; + display: flex; + align-items: center; + text-align: center; + justify-content: center; + padding: var(--spacing-double); + } } From 9a3c29b5b8031ea7271d2b9d822cffd1471fe68d Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 28 Sep 2022 09:04:53 -0400 Subject: [PATCH 040/262] Add no changes verbiage --- .../open-pull-request-dialog.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index fe449d15a9..95fb620209 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -5,6 +5,9 @@ import { ImageDiffType } from '../../models/diff' import { Repository } from '../../models/repository' import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog' import { Dispatcher } from '../dispatcher' +import { Ref } from '../lib/ref' +import { Octicon } from '../octicons' +import * as OcticonSymbol from '../octicons/octicons.generated' import { OpenPullRequestDialogHeader } from './open-pull-request-header' import { PullRequestFilesChanged } from './pull-request-files-changed' @@ -136,13 +139,24 @@ export class OpenPullRequestDialog extends React.Component No changes!
+ return ( +
+
+ +

There are no changes.

+ {baseBranch.name} is up to date with all commits from{' '} + {currentBranch.name}. +
+
+ ) } private renderFooter() { From c9d74f4a775608b0042167176a879458d0e2d5e4 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:15:37 -0400 Subject: [PATCH 041/262] Update app/src/lib/stores/app-store.ts Co-authored-by: Sergio Padrino --- app/src/lib/stores/app-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 9674327244..008e6f27b9 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -352,7 +352,7 @@ const hideWhitespaceInChangesDiffKey = 'hide-whitespace-in-changes-diff' const hideWhitespaceInHistoryDiffDefault = false const hideWhitespaceInHistoryDiffKey = 'hide-whitespace-in-diff' const hideWhitespaceInPullRequestDiffDefault = false -const hideWhitespaceInPullRequestDiffKey = 'hide-whitespace-in-diff' +const hideWhitespaceInPullRequestDiffKey = 'hide-whitespace-in-pull-request-diff' const commitSpellcheckEnabledDefault = true const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled' From 21d0a60c05152efbf05ec1cde7f656edc2479944 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:21:50 -0400 Subject: [PATCH 042/262] Don't need this to be a promise? --- app/src/ui/diff/diff-options.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/diff/diff-options.tsx b/app/src/ui/diff/diff-options.tsx index f5791f5e44..9196dd9317 100644 --- a/app/src/ui/diff/diff-options.tsx +++ b/app/src/ui/diff/diff-options.tsx @@ -10,7 +10,7 @@ interface IDiffOptionsProps { readonly hideWhitespaceChanges: boolean readonly onHideWhitespaceChangesChanged: ( hideWhitespaceChanges: boolean - ) => Promise | void + ) => void readonly showSideBySideDiff: boolean readonly onShowSideBySideDiffChanged: (showSideBySideDiff: boolean) => void From 42cd43b6d96aaddf5b95d23adbdf44aaf6462b8d Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:25:45 -0400 Subject: [PATCH 043/262] Linter.. --- app/src/lib/stores/app-store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 008e6f27b9..a38313233c 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -352,7 +352,8 @@ const hideWhitespaceInChangesDiffKey = 'hide-whitespace-in-changes-diff' const hideWhitespaceInHistoryDiffDefault = false const hideWhitespaceInHistoryDiffKey = 'hide-whitespace-in-diff' const hideWhitespaceInPullRequestDiffDefault = false -const hideWhitespaceInPullRequestDiffKey = 'hide-whitespace-in-pull-request-diff' +const hideWhitespaceInPullRequestDiffKey = + 'hide-whitespace-in-pull-request-diff' const commitSpellcheckEnabledDefault = true const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled' From cc69b05956d100aff772051e945f98657f71f0b5 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 26 Sep 2022 11:25:24 -0400 Subject: [PATCH 044/262] Add update method chain to app-store --- app/src/lib/stores/app-store.ts | 1 + app/src/ui/dispatcher/dispatcher.ts | 4 ++++ app/src/ui/open-pull-request/open-pull-request-dialog.tsx | 6 ++++++ app/src/ui/open-pull-request/open-pull-request-header.tsx | 5 +++++ 4 files changed, 16 insertions(+) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index a38313233c..caec31e2d3 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7377,6 +7377,7 @@ export class AppStore extends TypedBaseStore { return Promise.resolve() } + public _updatePullRequestBaseBranch(repository: Repository, branch: Branch) {} } /** diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index dec5515def..2641f3cfc1 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -4001,4 +4001,8 @@ export class Dispatcher { public resetPullRequestFileListWidth(): Promise { return this.appStore._resetPullRequestFileListWidth() } + + public updatePullRequestBaseBranch(repository: Repository, branch: Branch) { + this.appStore._updatePullRequestBaseBranch(repository, branch) + } } diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index c6991954b0..53ffd5fcf5 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -68,6 +68,11 @@ export class OpenPullRequestDialog extends React.Component { + const { repository } = this.props + this.props.dispatcher.updatePullRequestBaseBranch(repository, branch) + } + private renderHeader() { const { currentBranch, @@ -85,6 +90,7 @@ export class OpenPullRequestDialog extends React.Component ) diff --git a/app/src/ui/open-pull-request/open-pull-request-header.tsx b/app/src/ui/open-pull-request/open-pull-request-header.tsx index e6b6045308..b9e376387b 100644 --- a/app/src/ui/open-pull-request/open-pull-request-header.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-header.tsx @@ -30,6 +30,9 @@ interface IOpenPullRequestDialogHeaderProps { /** The count of commits of the pull request */ readonly commitCount: number + /** When the branch selection changes */ + readonly onBranchChange: (branch: Branch) => void + /** * Event triggered when the dialog is dismissed by the user in the * ways described in the dismissable prop. @@ -54,6 +57,7 @@ export class OpenPullRequestDialogHeader extends React.Component< allBranches, recentBranches, commitCount, + onBranchChange, onDismissed, } = this.props const commits = `${commitCount} commit${commitCount > 1 ? 's' : ''}` @@ -74,6 +78,7 @@ export class OpenPullRequestDialogHeader extends React.Component< currentBranch={currentBranch} allBranches={allBranches} recentBranches={recentBranches} + onChange={onBranchChange} />{' '} from {currentBranch.name}.
From c4d549387635072b0cb1d0bee2f067415f77f833 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 26 Sep 2022 12:48:29 -0400 Subject: [PATCH 045/262] Repeat pr start for new base branch ( move duplicate logic to new method) --- app/src/lib/stores/app-store.ts | 39 ++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index caec31e2d3..5ab929bc3d 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7215,12 +7215,19 @@ export class AppStore extends TypedBaseStore { if (defaultBranch === null || tip.kind !== TipState.Valid) { return } - const currentBranch = tip.branch + this._initializePullRequestPreview(repository, defaultBranch, currentBranch) + } + + private async _initializePullRequestPreview( + repository: Repository, + baseBranch: Branch, + currentBranch: Branch + ) { const gitStore = this.gitStoreCache.get(repository) const pullRequestCommits = await gitStore.getCommitsBetweenBranches( - defaultBranch, + baseBranch, currentBranch ) @@ -7233,7 +7240,7 @@ export class AppStore extends TypedBaseStore { ? await gitStore.performFailableOperation(() => getBranchMergeBaseChangedFiles( repository, - defaultBranch.name, + baseBranch.name, currentBranch.name, commitSHAs[0] ) @@ -7245,7 +7252,7 @@ export class AppStore extends TypedBaseStore { } this.repositoryStateCache.initializePullRequestState(repository, { - baseBranch: defaultBranch, + baseBranch, commitSHAs, commitSelection: { shas: commitSHAs, @@ -7257,6 +7264,8 @@ export class AppStore extends TypedBaseStore { }, }) + this.emitUpdate() + if (changesetData.files.length > 0) { await this._changePullRequestFileSelection( repository, @@ -7377,7 +7386,27 @@ export class AppStore extends TypedBaseStore { return Promise.resolve() } - public _updatePullRequestBaseBranch(repository: Repository, branch: Branch) {} + + 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) + } } /** From bf9e2422e719f5b2b77a280a87df5bb02586dbb7 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 29 Sep 2022 10:40:07 -0400 Subject: [PATCH 046/262] Fix update from dev changes --- app/src/lib/stores/app-store.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 5ab929bc3d..7dea0b780d 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7208,8 +7208,7 @@ export class AppStore extends TypedBaseStore { } public async _startPullRequest(repository: Repository) { - const { branchesState, localCommitSHAs } = - this.repositoryStateCache.get(repository) + const { branchesState } = this.repositoryStateCache.get(repository) const { defaultBranch, tip } = branchesState if (defaultBranch === null || tip.kind !== TipState.Valid) { @@ -7224,6 +7223,8 @@ export class AppStore extends TypedBaseStore { baseBranch: Branch, currentBranch: Branch ) { + const { branchesState, localCommitSHAs } = + this.repositoryStateCache.get(repository) const gitStore = this.gitStoreCache.get(repository) const pullRequestCommits = await gitStore.getCommitsBetweenBranches( @@ -7273,7 +7274,7 @@ export class AppStore extends TypedBaseStore { ) } - const { allBranches, recentBranches } = branchesState + const { allBranches, recentBranches, defaultBranch } = branchesState const { imageDiffType, selectedExternalEditor, showSideBySideDiff } = this.getState() From 1b76c6fc8c93e1dd026b6abbbd5d3cd9aa827238 Mon Sep 17 00:00:00 2001 From: Rebecca Miller-Webster Date: Thu, 29 Sep 2022 15:52:37 -0500 Subject: [PATCH 047/262] Update setup.md Update expected node, yarn, and python versions --- docs/contributing/setup.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/contributing/setup.md b/docs/contributing/setup.md index 2b1f3d7020..05a6819e98 100644 --- a/docs/contributing/setup.md +++ b/docs/contributing/setup.md @@ -19,13 +19,13 @@ versions look similar to the below output: ```shellsession $ node -v -v10.15.4 +v16.13.0 $ yarn -v -1.15.2 +1.21.1 $ python --version -Python 2.7.15 +Python 3.9.x ``` There are also [additional resources](tooling.md) to configure your favorite From f18ce24e9695a99d34ee68dee93a1d233b182dde Mon Sep 17 00:00:00 2001 From: Tsvetilian Yankov Date: Fri, 30 Sep 2022 12:49:38 +0300 Subject: [PATCH 048/262] Add discard stash toggle option to the prompt --- app/src/ui/app.tsx | 3 +++ app/src/ui/stashing/confirm-discard-stash.tsx | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 6609225ced..306899a670 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -1850,6 +1850,9 @@ export class App extends React.Component { void } interface IConfirmDiscardStashState { readonly isDiscarding: boolean + readonly confirmDiscardStash: boolean } /** * Dialog to confirm dropping a stash @@ -28,6 +31,7 @@ export class ConfirmDiscardStashDialog extends React.Component< this.state = { isDiscarding: false, + confirmDiscardStash: props.askForConfirmationOnDiscardStash, } } @@ -46,6 +50,17 @@ export class ConfirmDiscardStashDialog extends React.Component< > Are you sure you want to discard these stashed changes? + + + @@ -54,6 +69,14 @@ export class ConfirmDiscardStashDialog extends React.Component< ) } + private onAskForConfirmationOnDiscardStashChanged = ( + event: React.FormEvent + ) => { + const value = !event.currentTarget.checked + + this.setState({ confirmDiscardStash: value }) + } + private onSubmit = async () => { const { dispatcher, repository, stash, onDismissed } = this.props @@ -62,6 +85,7 @@ export class ConfirmDiscardStashDialog extends React.Component< }) try { + dispatcher.setConfirmDiscardStashSetting(this.state.confirmDiscardStash) await dispatcher.dropStash(repository, stash) } finally { this.setState({ From 29c161d077a036d458efc9679d7449c7e11adfed Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Fri, 30 Sep 2022 18:03:52 +0200 Subject: [PATCH 049/262] Use a more accurate method name --- app/src/main-process/app-window.ts | 2 +- app/src/main-process/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main-process/app-window.ts b/app/src/main-process/app-window.ts index 073509acac..96d2855c23 100644 --- a/app/src/main-process/app-window.ts +++ b/app/src/main-process/app-window.ts @@ -213,7 +213,7 @@ export class AppWindow { return !!this.loadTime && !!this.rendererReadyTime } - public onClose(fn: () => void) { + public onClosed(fn: () => void) { this.window.on('closed', fn) } diff --git a/app/src/main-process/main.ts b/app/src/main-process/main.ts index 114480145f..6107895ca5 100644 --- a/app/src/main-process/main.ts +++ b/app/src/main-process/main.ts @@ -738,7 +738,7 @@ function createWindow() { } } - window.onClose(() => { + window.onClosed(() => { mainWindow = null if (!__DARWIN__ && !preventQuit) { app.quit() From 8b1c2dee62a8dc31d7d50c9033937cbaab79ff8c Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Fri, 30 Sep 2022 18:04:42 +0200 Subject: [PATCH 050/262] First steps of an "installing update" dialog --- app/src/lib/ipc-shared.ts | 1 + app/src/lib/stores/app-store.ts | 9 ++ app/src/models/popup.ts | 4 + app/src/ui/app.tsx | 9 ++ .../installing-update/installing-update.tsx | 140 ++++++++++++++++++ app/src/ui/main-process-proxy.ts | 6 + 6 files changed, 169 insertions(+) create mode 100644 app/src/ui/installing-update/installing-update.tsx diff --git a/app/src/lib/ipc-shared.ts b/app/src/lib/ipc-shared.ts index 07ffdb7cc0..dae329f719 100644 --- a/app/src/lib/ipc-shared.ts +++ b/app/src/lib/ipc-shared.ts @@ -77,6 +77,7 @@ export type RequestChannels = { 'focus-window': () => void 'notification-event': NotificationCallback 'set-window-zoom-factor': (zoomFactor: number) => void + 'show-installing-update': () => void } /** diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index a38313233c..6856656c89 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -77,6 +77,7 @@ import { updatePreferredAppMenuItemLabels, updateAccounts, setWindowZoomFactor, + onShowInstallingUpdate, } from '../../ui/main-process-proxy' import { API, @@ -579,6 +580,8 @@ export class AppStore extends TypedBaseStore { this.notificationsStore.onPullRequestReviewSubmitNotification( this.onPullRequestReviewSubmitNotification ) + + onShowInstallingUpdate(this.onShowInstallingUpdate) } private initializeWindowState = async () => { @@ -649,6 +652,12 @@ export class AppStore extends TypedBaseStore { }) } + private onShowInstallingUpdate = () => { + this._showPopup({ + type: PopupType.InstallingUpdate, + }) + } + /** Figure out what step of the tutorial the user needs to do next */ private async updateCurrentTutorialStep( repository: Repository diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 57cc87c866..fe2f1df53f 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -87,6 +87,7 @@ export enum PopupType { PullRequestReview, UnreachableCommits, StartPullRequest, + InstallingUpdate, } export type Popup = @@ -372,3 +373,6 @@ export type Popup = nonLocalCommitSHA: string | null showSideBySideDiff: boolean } + | { + type: PopupType.InstallingUpdate + } diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index d6cd421e39..8adb22fd01 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -159,6 +159,7 @@ import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog' import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-dialog' import { sendNonFatalException } from '../lib/helpers/non-fatal-exception' import { createCommitURL } from '../lib/commit-url' +import { InstallingUpdate } from './installing-update/installing-update' const MinuteInMilliseconds = 1000 * 60 const HourInMilliseconds = MinuteInMilliseconds * 60 @@ -2294,6 +2295,14 @@ export class App extends React.Component { /> ) } + case PopupType.InstallingUpdate: { + return ( + + ) + } default: return assertNever(popup, `Unknown popup type: ${popup}`) } diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx new file mode 100644 index 0000000000..57e2cb9fc4 --- /dev/null +++ b/app/src/ui/installing-update/installing-update.tsx @@ -0,0 +1,140 @@ +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 { Loading } from '../lib/loading' +import { assertNever } from '../../lib/fatal-error' +import { closeWindow, sendWillQuitSync } from '../main-process-proxy' +import { DialogHeader } from '../dialog/header' + +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 +} + +interface IInstallingUpdateState { + readonly updateState: IUpdateState +} + +/** + * A dialog that presents information about the + * running application such as name and version. + */ +export class InstallingUpdate extends React.Component< + IInstallingUpdateProps, + IInstallingUpdateState +> { + private updateStoreEventHandle: Disposable | null = null + + public constructor(props: IInstallingUpdateProps) { + super(props) + + this.state = { + updateState: updateStore.state, + } + } + + private onUpdateStateChanged = (updateState: IUpdateState) => { + this.setState({ updateState }) + + // If the update is not being downloaded (`UpdateStatus.UpdateAvailable`), + // i.e. if it's already downloaded or not available, close the window. + closeWindow() + if (updateState.status !== UpdateStatus.UpdateAvailable) { + } + } + + 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 + } + } + + private renderUpdateAvailable() { + return ( + + + + Installing update… please, do not close GitHub Desktop until the + update is completely installed. + + + ) + } + + private renderUpdateReady() { + return ( +

+ An update has been downloaded, you can close GitHub Desktop now. +

+ ) + } + + private renderUpdateDetails() { + const updateState = this.state.updateState + + switch (updateState.status) { + case UpdateStatus.UpdateAvailable: + return this.renderUpdateAvailable() + case UpdateStatus.UpdateReady: + return this.renderUpdateReady() + case UpdateStatus.CheckingForUpdates: + case UpdateStatus.UpdateNotAvailable: + case UpdateStatus.UpdateNotChecked: + return null + default: + return assertNever( + updateState.status, + `Unknown update status ${updateState.status}` + ) + } + } + + private onQuitAnywayButtonClicked = () => { + sendWillQuitSync() + closeWindow() + } + + public render() { + return ( + + + {this.renderUpdateDetails()} + + + + + ) + } +} diff --git a/app/src/ui/main-process-proxy.ts b/app/src/ui/main-process-proxy.ts index 710c04378c..5ebbf7442a 100644 --- a/app/src/ui/main-process-proxy.ts +++ b/app/src/ui/main-process-proxy.ts @@ -200,6 +200,12 @@ export function onNativeThemeUpdated(eventHandler: () => void) { ipcRenderer.on('native-theme-updated', eventHandler) } +/** Subscribes to the "show installing update dialog" event originating from the + * main process */ +export function onShowInstallingUpdate(eventHandler: () => void) { + ipcRenderer.on('show-installing-update', eventHandler) +} + /** Tell the main process to set the native theme source */ export const setNativeThemeSource = sendProxy('set-native-theme-source', 1) From 841ea8194b322b4de497e1d8e3a12308d4d0842c Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Fri, 30 Sep 2022 18:16:09 +0200 Subject: [PATCH 051/262] Attempting to quit the app --- app/src/lib/ipc-shared.ts | 1 + app/src/main-process/app-window.ts | 22 +++++++++++++++++++ .../installing-update/installing-update.tsx | 14 +++++++++--- app/src/ui/lib/update-store.ts | 2 +- app/src/ui/main-process-proxy.ts | 12 ++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/app/src/lib/ipc-shared.ts b/app/src/lib/ipc-shared.ts index dae329f719..aa84deb7ab 100644 --- a/app/src/lib/ipc-shared.ts +++ b/app/src/lib/ipc-shared.ts @@ -46,6 +46,7 @@ export type RequestChannels = { 'menu-event': (name: MenuEvent) => void log: (level: LogLevel, message: string) => void 'will-quit': () => void + 'will-quit-even-updating': () => void 'crash-ready': () => void 'crash-quit': () => void 'window-state-changed': (windowState: WindowState) => void diff --git a/app/src/main-process/app-window.ts b/app/src/main-process/app-window.ts index 96d2855c23..84a1ffd880 100644 --- a/app/src/main-process/app-window.ts +++ b/app/src/main-process/app-window.ts @@ -33,6 +33,7 @@ export class AppWindow { private _loadTime: number | null = null private _rendererReadyTime: number | null = null + private isDownloadingUpdate: boolean = false private minWidth = 960 private minHeight = 660 @@ -86,6 +87,7 @@ export class AppWindow { this.shouldMaximizeOnShow = savedWindowState.isMaximized let quitting = false + let quittingEvenDownloadingUpdate = false app.on('before-quit', () => { quitting = true }) @@ -95,7 +97,22 @@ export class AppWindow { event.returnValue = true }) + ipcMain.on('will-quit-even-updating', event => { + quitting = true + quittingEvenDownloadingUpdate = true + event.returnValue = true + }) + this.window.on('close', e => { + if ( + !quittingEvenDownloadingUpdate && + (1 !== NaN || this.isDownloadingUpdate) + ) { + e.preventDefault() + ipcWebContents.send(this.window.webContents, 'show-installing-update') + return + } + // on macOS, when the user closes the window we really just hide it. This // lets us activate quickly and keep all our interesting logic in the // renderer. @@ -344,10 +361,12 @@ export class AppWindow { public setupAutoUpdater() { autoUpdater.on('error', (error: Error) => { + this.isDownloadingUpdate = false ipcWebContents.send(this.window.webContents, 'auto-updater-error', error) }) autoUpdater.on('checking-for-update', () => { + this.isDownloadingUpdate = false ipcWebContents.send( this.window.webContents, 'auto-updater-checking-for-update' @@ -355,6 +374,7 @@ export class AppWindow { }) autoUpdater.on('update-available', () => { + this.isDownloadingUpdate = true ipcWebContents.send( this.window.webContents, 'auto-updater-update-available' @@ -362,6 +382,7 @@ export class AppWindow { }) autoUpdater.on('update-not-available', () => { + this.isDownloadingUpdate = false ipcWebContents.send( this.window.webContents, 'auto-updater-update-not-available' @@ -369,6 +390,7 @@ export class AppWindow { }) autoUpdater.on('update-downloaded', () => { + this.isDownloadingUpdate = false ipcWebContents.send( this.window.webContents, 'auto-updater-update-downloaded' diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index 57e2cb9fc4..4e1158252c 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -11,8 +11,13 @@ import { updateStore, IUpdateState, UpdateStatus } from '../lib/update-store' import { Disposable } from 'event-kit' import { Loading } from '../lib/loading' import { assertNever } from '../../lib/fatal-error' -import { closeWindow, sendWillQuitSync } from '../main-process-proxy' +import { + closeWindow, + sendWillQuitEvenUpdatingSync, + sendWillQuitSync, +} from '../main-process-proxy' import { DialogHeader } from '../dialog/header' +import { app } from 'electron' interface IInstallingUpdateProps { /** @@ -49,8 +54,10 @@ export class InstallingUpdate extends React.Component< // If the update is not being downloaded (`UpdateStatus.UpdateAvailable`), // i.e. if it's already downloaded or not available, close the window. - closeWindow() if (updateState.status !== UpdateStatus.UpdateAvailable) { + sendWillQuitSync() + closeWindow() + app.quit() } } @@ -111,8 +118,9 @@ export class InstallingUpdate extends React.Component< } private onQuitAnywayButtonClicked = () => { - sendWillQuitSync() + sendWillQuitEvenUpdatingSync() closeWindow() + app.quit() } public render() { diff --git a/app/src/ui/lib/update-store.ts b/app/src/ui/lib/update-store.ts index e4ad40d9ae..00e8c71c62 100644 --- a/app/src/ui/lib/update-store.ts +++ b/app/src/ui/lib/update-store.ts @@ -56,7 +56,7 @@ export interface IUpdateState { /** A store which contains the current state of the auto updater. */ class UpdateStore { private emitter = new Emitter() - private status = UpdateStatus.UpdateNotChecked + private status = UpdateStatus.UpdateAvailable // UpdateStatus.UpdateNotChecked private lastSuccessfulCheck: Date | null = null private newReleases: ReadonlyArray | null = null private isX64ToARM64ImmediateAutoUpdate: boolean = false diff --git a/app/src/ui/main-process-proxy.ts b/app/src/ui/main-process-proxy.ts index 5ebbf7442a..270ddfc8b9 100644 --- a/app/src/ui/main-process-proxy.ts +++ b/app/src/ui/main-process-proxy.ts @@ -279,6 +279,18 @@ export function sendWillQuitSync() { ipcRenderer.sendSync('will-quit') } +/** + * Tell the main process that we're going to quit, even if the app is installing + * an update. This means it should allow the window to close. + * + * This event is sent synchronously to avoid any races with subsequent calls + * that would tell the app to quit. + */ +export function sendWillQuitEvenUpdatingSync() { + // eslint-disable-next-line no-sync + ipcRenderer.sendSync('will-quit-even-updating') +} + /** * Tell the main process to move the application to the application folder */ From 73fcceb2f42136f89b0e009a875ae966901d2415 Mon Sep 17 00:00:00 2001 From: Sathvik Shanmugam Date: Sun, 2 Oct 2022 16:05:37 +0530 Subject: [PATCH 052/262] Adding new levels in zooming --- app/src/main-process/menu/build-default-menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main-process/menu/build-default-menu.ts b/app/src/main-process/menu/build-default-menu.ts index b01d63f30c..2186d80500 100644 --- a/app/src/main-process/menu/build-default-menu.ts +++ b/app/src/main-process/menu/build-default-menu.ts @@ -640,7 +640,7 @@ function emit(name: MenuEvent): ClickHandler { } /** The zoom steps that we support, these factors must sorted */ -const ZoomInFactors = [1, 1.1, 1.25, 1.5, 1.75, 2] +const ZoomInFactors = [0.5, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2] const ZoomOutFactors = ZoomInFactors.slice().reverse() /** From 6ad7ec70b8b9ee42f10ee3a07d03a3bf4f07342e Mon Sep 17 00:00:00 2001 From: Sathvik Shanmugam Date: Sun, 2 Oct 2022 16:44:40 +0530 Subject: [PATCH 053/262] Adding levels to get sync with the browser standards --- app/src/main-process/menu/build-default-menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main-process/menu/build-default-menu.ts b/app/src/main-process/menu/build-default-menu.ts index 2186d80500..a661eaa78a 100644 --- a/app/src/main-process/menu/build-default-menu.ts +++ b/app/src/main-process/menu/build-default-menu.ts @@ -640,7 +640,7 @@ function emit(name: MenuEvent): ClickHandler { } /** The zoom steps that we support, these factors must sorted */ -const ZoomInFactors = [0.5, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2] +const ZoomInFactors = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2] const ZoomOutFactors = ZoomInFactors.slice().reverse() /** From 55f994e7f3ed92cd1a90d17b62c2f0894f8f9fe2 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 3 Oct 2022 07:46:04 -0400 Subject: [PATCH 054/262] Make look like a button - add label --- app/src/ui/branches/branch-select.tsx | 40 +++++++++++++-------------- app/src/ui/lib/popover-dropdown.tsx | 4 ++- app/styles/ui/_branch-select.scss | 9 ------ app/styles/ui/_popover-dropdown.scss | 23 +++------------ 4 files changed, 26 insertions(+), 50 deletions(-) diff --git a/app/src/ui/branches/branch-select.tsx b/app/src/ui/branches/branch-select.tsx index 4cde775e46..e79b5c6742 100644 --- a/app/src/ui/branches/branch-select.tsx +++ b/app/src/ui/branches/branch-select.tsx @@ -80,27 +80,25 @@ export class BranchSelect extends React.Component< const { filterText, selectedBranch } = this.state return ( -
- base: - - - -
+ + + ) } } diff --git a/app/src/ui/lib/popover-dropdown.tsx b/app/src/ui/lib/popover-dropdown.tsx index d2e18518d1..7ba6233617 100644 --- a/app/src/ui/lib/popover-dropdown.tsx +++ b/app/src/ui/lib/popover-dropdown.tsx @@ -12,6 +12,7 @@ interface IPopoverDropdownProps { readonly className?: string readonly contentTitle: string readonly buttonContent: JSX.Element | string + readonly label: string } interface IPopoverDropdownState { @@ -112,7 +113,7 @@ export class PopoverDropdown extends React.Component< } public render() { - const { className, buttonContent } = this.props + const { className, buttonContent, label } = this.props const cn = classNames('popover-dropdown-component', className) return ( @@ -121,6 +122,7 @@ export class PopoverDropdown extends React.Component< onClick={this.togglePopover} onButtonRef={this.onInvokeButtonRef} > + {label} {buttonContent} diff --git a/app/styles/ui/_branch-select.scss b/app/styles/ui/_branch-select.scss index c0ae93fd27..e69de29bb2 100644 --- a/app/styles/ui/_branch-select.scss +++ b/app/styles/ui/_branch-select.scss @@ -1,9 +0,0 @@ -.branch-select-component { - display: inline; - - .base-label { - font-weight: var(--font-weight-semibold); - color: var(--text-secondary-color); - margin: 0 var(--spacing-half); - } -} diff --git a/app/styles/ui/_popover-dropdown.scss b/app/styles/ui/_popover-dropdown.scss index 178674cb77..6a0ccdba23 100644 --- a/app/styles/ui/_popover-dropdown.scss +++ b/app/styles/ui/_popover-dropdown.scss @@ -1,25 +1,10 @@ .popover-dropdown-component { display: inline-flex; - button { - border: none; - background-color: inherit; - border: none; - padding: 0; - margin: 0; - color: var(--link-button-color); - font-weight: bold; - text-decoration: underline; - - &.button-component { - overflow: visible; - - &:hover, - &:focus { - border: none; - box-shadow: none; - } - } + .popover-dropdown-button-label { + font-weight: var(--font-weight-semibold); + color: var(--text-secondary-color); + margin: 0 var(--spacing-half); } .popover-dropdown-popover { From 3ea43eb5a6ea8428d2a06100d4110e31b3c6c621 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 15:28:11 +0000 Subject: [PATCH 055/262] Bump peter-evans/create-pull-request from 4.1.2 to 4.1.3 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.1.2 to 4.1.3. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v4.1.2...v4.1.3) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 3b3811a498..c6daccbcec 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -37,7 +37,7 @@ jobs: private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} - name: Create Release Pull Request - uses: peter-evans/create-pull-request@v4.1.2 + uses: peter-evans/create-pull-request@v4.1.3 if: | startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test') with: From 16a6be7318e6a0cab86c680fe3214d14fb84a750 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Mon, 3 Oct 2022 18:00:31 +0200 Subject: [PATCH 056/262] Manage quit events properly --- app/src/lib/ipc-shared.ts | 2 ++ app/src/main-process/app-window.ts | 18 +++++++++++++++ app/src/main-process/main.ts | 2 ++ .../installing-update/installing-update.tsx | 22 +++++++++++-------- app/src/ui/main-process-proxy.ts | 14 ++++++++++++ 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/app/src/lib/ipc-shared.ts b/app/src/lib/ipc-shared.ts index aa84deb7ab..b653fa5936 100644 --- a/app/src/lib/ipc-shared.ts +++ b/app/src/lib/ipc-shared.ts @@ -47,6 +47,7 @@ export type RequestChannels = { log: (level: LogLevel, message: string) => void 'will-quit': () => void 'will-quit-even-updating': () => void + 'cancel-quit': () => void 'crash-ready': () => void 'crash-quit': () => void 'window-state-changed': (windowState: WindowState) => void @@ -64,6 +65,7 @@ export type RequestChannels = { blur: () => void 'update-accounts': (accounts: ReadonlyArray) => void 'quit-and-install-updates': () => void + 'quit-app': () => void 'minimize-window': () => void 'maximize-window': () => void 'unmaximize-window': () => void diff --git a/app/src/main-process/app-window.ts b/app/src/main-process/app-window.ts index 84a1ffd880..421b912ccd 100644 --- a/app/src/main-process/app-window.ts +++ b/app/src/main-process/app-window.ts @@ -103,13 +103,31 @@ export class AppWindow { event.returnValue = true }) + ipcMain.on('cancel-quit', event => { + quitting = false + quittingEvenDownloadingUpdate = false + event.returnValue = true + }) + this.window.on('close', e => { + // On macOS, closing the window doesn't mean the app is quitting. If the + // app is updating, we will prevent the window from closing only when the + // app is also quitting. if ( + (!__DARWIN__ || quitting) && !quittingEvenDownloadingUpdate && + // TODO: DON'T MERGE THIS 1!== NaN!!!! (1 !== NaN || 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 } diff --git a/app/src/main-process/main.ts b/app/src/main-process/main.ts index 6107895ca5..640c3a4414 100644 --- a/app/src/main-process/main.ts +++ b/app/src/main-process/main.ts @@ -490,6 +490,8 @@ app.on('ready', () => { mainWindow?.quitAndInstallUpdate() ) + ipcMain.on('quit-app', () => app.quit()) + ipcMain.on('minimize-window', () => mainWindow?.minimizeWindow()) ipcMain.on('maximize-window', () => mainWindow?.maximizeWindow()) diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index 4e1158252c..7be857d8f2 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -12,12 +12,11 @@ import { Disposable } from 'event-kit' import { Loading } from '../lib/loading' import { assertNever } from '../../lib/fatal-error' import { - closeWindow, + quitApp, + sendCancelQuitSync, sendWillQuitEvenUpdatingSync, - sendWillQuitSync, } from '../main-process-proxy' import { DialogHeader } from '../dialog/header' -import { app } from 'electron' interface IInstallingUpdateProps { /** @@ -55,9 +54,7 @@ export class InstallingUpdate extends React.Component< // 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) { - sendWillQuitSync() - closeWindow() - app.quit() + quitApp() } } @@ -118,9 +115,16 @@ export class InstallingUpdate extends React.Component< } private onQuitAnywayButtonClicked = () => { + log.info('User chose to quit anyway') sendWillQuitEvenUpdatingSync() - closeWindow() - app.quit() + log.info('Quitting') + quitApp() + log.info('Quitted') + } + + private onCancel = () => { + sendCancelQuitSync() + this.props.onDismissed() } public render() { @@ -139,7 +143,7 @@ export class InstallingUpdate extends React.Component<
diff --git a/app/src/ui/main-process-proxy.ts b/app/src/ui/main-process-proxy.ts index 270ddfc8b9..9b4dc3ece7 100644 --- a/app/src/ui/main-process-proxy.ts +++ b/app/src/ui/main-process-proxy.ts @@ -164,6 +164,9 @@ export const checkForUpdates = invokeProxy('check-for-updates', 1) /** Tell the main process to quit the app and install updates */ export const quitAndInstallUpdate = sendProxy('quit-and-install-updates', 0) +/** Tell the main process to quit the app */ +export const quitApp = sendProxy('quit-app', 0) + /** Subscribes to auto updater error events originating from the main process */ export function onAutoUpdaterError( errorHandler: (evt: Electron.IpcRendererEvent, error: Error) => void @@ -291,6 +294,17 @@ export function sendWillQuitEvenUpdatingSync() { ipcRenderer.sendSync('will-quit-even-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 sendCancelQuitSync() { + // eslint-disable-next-line no-sync + ipcRenderer.sendSync('cancel-quit') +} + /** * Tell the main process to move the application to the application folder */ From d689c2592668886d9bab7168c2390136552b917e Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 3 Oct 2022 12:07:13 -0400 Subject: [PATCH 057/262] Semi bold button text --- app/styles/ui/_popover-dropdown.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/styles/ui/_popover-dropdown.scss b/app/styles/ui/_popover-dropdown.scss index 6a0ccdba23..6b8b20dafb 100644 --- a/app/styles/ui/_popover-dropdown.scss +++ b/app/styles/ui/_popover-dropdown.scss @@ -1,8 +1,12 @@ .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); } From 26b981b1ad37cca8afec15943d607bf6f0f0ccae Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 08:18:49 -0400 Subject: [PATCH 058/262] Use undo commit key --- app/src/lib/stores/app-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 7dea0b780d..8f8541a540 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -5261,7 +5261,7 @@ export class AppStore extends TypedBaseStore { public _setConfirmUndoCommitSetting(value: boolean): Promise { this.confirmUndoCommit = value - setBoolean(confirmForcePushKey, value) + setBoolean(confirmUndoCommitKey, value) this.emitUpdate() From f9365ef957c582e19622ec24ae7909f454b9b7d1 Mon Sep 17 00:00:00 2001 From: Tsvetilian Yankov Date: Tue, 4 Oct 2022 23:26:23 +0300 Subject: [PATCH 059/262] Fix typo Co-authored-by: Sergio Padrino --- app/src/lib/app-state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index d7277a281f..19a1cce3f9 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -194,7 +194,7 @@ export interface IAppState { /** Whether we should show a confirmation dialog */ readonly askForConfirmationOnDiscardChangesPermanently: boolean - /** Should the app propt the user to confirm a discard stash */ + /** 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? */ From f7ea1bcb0b16143713f84d835426f3e16aabd77e Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 5 Oct 2022 16:36:17 +0200 Subject: [PATCH 060/262] Be more explicit about code that must be reverted before merging --- app/src/ui/lib/update-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/lib/update-store.ts b/app/src/ui/lib/update-store.ts index 00e8c71c62..03d93b4aad 100644 --- a/app/src/ui/lib/update-store.ts +++ b/app/src/ui/lib/update-store.ts @@ -56,7 +56,7 @@ export interface IUpdateState { /** A store which contains the current state of the auto updater. */ class UpdateStore { private emitter = new Emitter() - private status = UpdateStatus.UpdateAvailable // UpdateStatus.UpdateNotChecked + private status = UpdateStatus.UpdateAvailable // // TODO: DON'T MERGE THIS, IT SHOULD BE UpdateStatus.UpdateNotChecked private lastSuccessfulCheck: Date | null = null private newReleases: ReadonlyArray | null = null private isX64ToARM64ImmediateAutoUpdate: boolean = false From 1e7b08caf26918283b882e1e8d98e3dc6d1ac124 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 5 Oct 2022 16:38:57 +0200 Subject: [PATCH 061/262] Use proper English :lol: Co-Authored-By: Markus Olsson <634063+niik@users.noreply.github.com> --- app/src/lib/ipc-shared.ts | 2 +- app/src/main-process/app-window.ts | 10 +++++----- app/src/ui/main-process-proxy.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/lib/ipc-shared.ts b/app/src/lib/ipc-shared.ts index b653fa5936..dba95a0688 100644 --- a/app/src/lib/ipc-shared.ts +++ b/app/src/lib/ipc-shared.ts @@ -46,7 +46,7 @@ export type RequestChannels = { 'menu-event': (name: MenuEvent) => void log: (level: LogLevel, message: string) => void 'will-quit': () => void - 'will-quit-even-updating': () => void + 'will-quit-even-if-updating': () => void 'cancel-quit': () => void 'crash-ready': () => void 'crash-quit': () => void diff --git a/app/src/main-process/app-window.ts b/app/src/main-process/app-window.ts index 421b912ccd..6d1b6a990d 100644 --- a/app/src/main-process/app-window.ts +++ b/app/src/main-process/app-window.ts @@ -87,7 +87,7 @@ export class AppWindow { this.shouldMaximizeOnShow = savedWindowState.isMaximized let quitting = false - let quittingEvenDownloadingUpdate = false + let quittingEvenIfUpdating = false app.on('before-quit', () => { quitting = true }) @@ -97,15 +97,15 @@ export class AppWindow { event.returnValue = true }) - ipcMain.on('will-quit-even-updating', event => { + ipcMain.on('will-quit-even-if-updating', event => { quitting = true - quittingEvenDownloadingUpdate = true + quittingEvenIfUpdating = true event.returnValue = true }) ipcMain.on('cancel-quit', event => { quitting = false - quittingEvenDownloadingUpdate = false + quittingEvenIfUpdating = false event.returnValue = true }) @@ -115,7 +115,7 @@ export class AppWindow { // app is also quitting. if ( (!__DARWIN__ || quitting) && - !quittingEvenDownloadingUpdate && + !quittingEvenIfUpdating && // TODO: DON'T MERGE THIS 1!== NaN!!!! (1 !== NaN || this.isDownloadingUpdate) ) { diff --git a/app/src/ui/main-process-proxy.ts b/app/src/ui/main-process-proxy.ts index 9b4dc3ece7..d8436d80f7 100644 --- a/app/src/ui/main-process-proxy.ts +++ b/app/src/ui/main-process-proxy.ts @@ -291,7 +291,7 @@ export function sendWillQuitSync() { */ export function sendWillQuitEvenUpdatingSync() { // eslint-disable-next-line no-sync - ipcRenderer.sendSync('will-quit-even-updating') + ipcRenderer.sendSync('will-quit-even-if-updating') } /** From dd4dd3d42b06f80235718e8212b365fd351dd1ce Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 5 Oct 2022 16:39:32 +0200 Subject: [PATCH 062/262] Remove unnecessary logging --- app/src/ui/installing-update/installing-update.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index 7be857d8f2..20a033ab37 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -115,11 +115,8 @@ export class InstallingUpdate extends React.Component< } private onQuitAnywayButtonClicked = () => { - log.info('User chose to quit anyway') sendWillQuitEvenUpdatingSync() - log.info('Quitting') quitApp() - log.info('Quitted') } private onCancel = () => { From f575c3f5c016c1142a343071630fa7009fba3b42 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 5 Oct 2022 16:40:53 +0200 Subject: [PATCH 063/262] User proper English (part 2) Co-Authored-By: Markus Olsson <634063+niik@users.noreply.github.com> --- app/src/ui/installing-update/installing-update.tsx | 4 ++-- app/src/ui/main-process-proxy.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index 20a033ab37..800f25c68a 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -14,7 +14,7 @@ import { assertNever } from '../../lib/fatal-error' import { quitApp, sendCancelQuitSync, - sendWillQuitEvenUpdatingSync, + sendWillQuitEvenIfUpdatingSync, } from '../main-process-proxy' import { DialogHeader } from '../dialog/header' @@ -115,7 +115,7 @@ export class InstallingUpdate extends React.Component< } private onQuitAnywayButtonClicked = () => { - sendWillQuitEvenUpdatingSync() + sendWillQuitEvenIfUpdatingSync() quitApp() } diff --git a/app/src/ui/main-process-proxy.ts b/app/src/ui/main-process-proxy.ts index d8436d80f7..d1f74c717d 100644 --- a/app/src/ui/main-process-proxy.ts +++ b/app/src/ui/main-process-proxy.ts @@ -289,7 +289,7 @@ export function sendWillQuitSync() { * This event is sent synchronously to avoid any races with subsequent calls * that would tell the app to quit. */ -export function sendWillQuitEvenUpdatingSync() { +export function sendWillQuitEvenIfUpdatingSync() { // eslint-disable-next-line no-sync ipcRenderer.sendSync('will-quit-even-if-updating') } From 426c834a117311f255cc303de28ac9c041c1242b Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 5 Oct 2022 16:50:07 +0200 Subject: [PATCH 064/262] Extract quitting actions to the Dispatcher/AppStore --- app/src/lib/ipc-shared.ts | 2 +- app/src/lib/stores/app-store.ts | 15 ++++++++++++++ app/src/main-process/app-window.ts | 2 +- app/src/ui/dispatcher/dispatcher.ts | 20 +++++++++++++++++++ .../installing-update/installing-update.tsx | 15 ++++++-------- app/src/ui/main-process-proxy.ts | 4 ++-- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/app/src/lib/ipc-shared.ts b/app/src/lib/ipc-shared.ts index dba95a0688..a8d4b3328a 100644 --- a/app/src/lib/ipc-shared.ts +++ b/app/src/lib/ipc-shared.ts @@ -47,7 +47,7 @@ export type RequestChannels = { log: (level: LogLevel, message: string) => void 'will-quit': () => void 'will-quit-even-if-updating': () => void - 'cancel-quit': () => void + 'cancel-quitting': () => void 'crash-ready': () => void 'crash-quit': () => void 'window-state-changed': (windowState: WindowState) => void diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 6856656c89..eefe329d99 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -78,6 +78,9 @@ import { updateAccounts, setWindowZoomFactor, onShowInstallingUpdate, + sendWillQuitEvenIfUpdatingSync, + quitApp, + sendCancelQuittingSync, } from '../../ui/main-process-proxy' import { API, @@ -7386,6 +7389,18 @@ export class AppStore extends TypedBaseStore { return Promise.resolve() } + + public _quitApp(evenIfUpdating: boolean) { + if (evenIfUpdating) { + sendWillQuitEvenIfUpdatingSync() + } + + quitApp() + } + + public _cancelQuittingApp() { + sendCancelQuittingSync() + } } /** diff --git a/app/src/main-process/app-window.ts b/app/src/main-process/app-window.ts index 6d1b6a990d..369a1039e8 100644 --- a/app/src/main-process/app-window.ts +++ b/app/src/main-process/app-window.ts @@ -103,7 +103,7 @@ export class AppWindow { event.returnValue = true }) - ipcMain.on('cancel-quit', event => { + ipcMain.on('cancel-quitting', event => { quitting = false quittingEvenIfUpdating = false event.returnValue = true diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index dec5515def..2ff714d4f2 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -4001,4 +4001,24 @@ export class Dispatcher { public resetPullRequestFileListWidth(): Promise { return this.appStore._resetPullRequestFileListWidth() } + + /** + * 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() + } } diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index 800f25c68a..fdba0cd600 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -11,12 +11,8 @@ import { updateStore, IUpdateState, UpdateStatus } from '../lib/update-store' import { Disposable } from 'event-kit' import { Loading } from '../lib/loading' import { assertNever } from '../../lib/fatal-error' -import { - quitApp, - sendCancelQuitSync, - sendWillQuitEvenIfUpdatingSync, -} from '../main-process-proxy' import { DialogHeader } from '../dialog/header' +import { Dispatcher } from '../dispatcher' interface IInstallingUpdateProps { /** @@ -24,6 +20,8 @@ interface IInstallingUpdateProps { * ways described in the Dialog component's dismissable prop. */ readonly onDismissed: () => void + + readonly dispatcher: Dispatcher } interface IInstallingUpdateState { @@ -54,7 +52,7 @@ export class InstallingUpdate extends React.Component< // 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) { - quitApp() + this.props.dispatcher.quitApp(false) } } @@ -115,12 +113,11 @@ export class InstallingUpdate extends React.Component< } private onQuitAnywayButtonClicked = () => { - sendWillQuitEvenIfUpdatingSync() - quitApp() + this.props.dispatcher.quitApp(true) } private onCancel = () => { - sendCancelQuitSync() + this.props.dispatcher.cancelQuittingApp() this.props.onDismissed() } diff --git a/app/src/ui/main-process-proxy.ts b/app/src/ui/main-process-proxy.ts index d1f74c717d..7d43837163 100644 --- a/app/src/ui/main-process-proxy.ts +++ b/app/src/ui/main-process-proxy.ts @@ -300,9 +300,9 @@ export function sendWillQuitEvenIfUpdatingSync() { * This event is sent synchronously to avoid any races with subsequent calls * that would tell the app to quit. */ -export function sendCancelQuitSync() { +export function sendCancelQuittingSync() { // eslint-disable-next-line no-sync - ipcRenderer.sendSync('cancel-quit') + ipcRenderer.sendSync('cancel-quitting') } /** From 49f4a64a8df22d35e2e19cf5e32d45b9414e230d Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 5 Oct 2022 17:19:40 +0200 Subject: [PATCH 065/262] =?UTF-8?q?Don't=20forget=20the=20dispatcher?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/ui/app.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 8adb22fd01..44f86ba52f 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2299,6 +2299,7 @@ export class App extends React.Component { return ( ) From c8ea1957af26f400c7ce609779d957aea9df03d3 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 5 Oct 2022 17:40:48 +0200 Subject: [PATCH 066/262] Improve UI --- .../installing-update/installing-update.tsx | 68 +++---------------- app/styles/ui/_dialog.scss | 1 + app/styles/ui/dialogs/_installing-update.scss | 7 ++ 3 files changed, 19 insertions(+), 57 deletions(-) create mode 100644 app/styles/ui/dialogs/_installing-update.scss diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index fdba0cd600..308381cabc 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -10,7 +10,6 @@ import { import { updateStore, IUpdateState, UpdateStatus } from '../lib/update-store' import { Disposable } from 'event-kit' import { Loading } from '../lib/loading' -import { assertNever } from '../../lib/fatal-error' import { DialogHeader } from '../dialog/header' import { Dispatcher } from '../dispatcher' @@ -24,31 +23,18 @@ interface IInstallingUpdateProps { readonly dispatcher: Dispatcher } -interface IInstallingUpdateState { - readonly updateState: IUpdateState -} - /** * A dialog that presents information about the * running application such as name and version. */ -export class InstallingUpdate extends React.Component< - IInstallingUpdateProps, - IInstallingUpdateState -> { +export class InstallingUpdate extends React.Component { private updateStoreEventHandle: Disposable | null = null public constructor(props: IInstallingUpdateProps) { super(props) - - this.state = { - updateState: updateStore.state, - } } private onUpdateStateChanged = (updateState: IUpdateState) => { - this.setState({ updateState }) - // 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) { @@ -72,46 +58,6 @@ export class InstallingUpdate extends React.Component< } } - private renderUpdateAvailable() { - return ( - - - - Installing update… please, do not close GitHub Desktop until the - update is completely installed. - - - ) - } - - private renderUpdateReady() { - return ( -

- An update has been downloaded, you can close GitHub Desktop now. -

- ) - } - - private renderUpdateDetails() { - const updateState = this.state.updateState - - switch (updateState.status) { - case UpdateStatus.UpdateAvailable: - return this.renderUpdateAvailable() - case UpdateStatus.UpdateReady: - return this.renderUpdateReady() - case UpdateStatus.CheckingForUpdates: - case UpdateStatus.UpdateNotAvailable: - case UpdateStatus.UpdateNotChecked: - return null - default: - return assertNever( - updateState.status, - `Unknown update status ${updateState.status}` - ) - } - } - private onQuitAnywayButtonClicked = () => { this.props.dispatcher.quitApp(true) } @@ -129,10 +75,18 @@ export class InstallingUpdate extends React.Component< dismissable={false} > - {this.renderUpdateDetails()} + + + + + Please, do not close GitHub Desktop until the update is completely + installed. + + + Date: Thu, 6 Oct 2022 10:53:40 +0530 Subject: [PATCH 067/262] Addressing review comments --- app/src/main-process/menu/build-default-menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main-process/menu/build-default-menu.ts b/app/src/main-process/menu/build-default-menu.ts index a661eaa78a..5da9141700 100644 --- a/app/src/main-process/menu/build-default-menu.ts +++ b/app/src/main-process/menu/build-default-menu.ts @@ -640,7 +640,7 @@ function emit(name: MenuEvent): ClickHandler { } /** The zoom steps that we support, these factors must sorted */ -const ZoomInFactors = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 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() /** From 7a9ac0f2f987ac7fafd0f9f652cab0c28c118f12 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 08:13:30 -0400 Subject: [PATCH 068/262] Add merge status to pull request state --- app/src/lib/app-state.ts | 3 ++ app/src/lib/stores/app-store.ts | 66 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index 1dd95f3a39..08ead91bc3 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -974,4 +974,7 @@ export interface IPullRequestState { * diff between the latest commit and the earliest commits parent. */ readonly commitSelection: ICommitSelection + + /** The result of merging the pull request branch into the base branch */ + readonly mergeStatus: MergeTreeResult | null } diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index f58777362c..8c8ea53bf2 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -473,6 +473,7 @@ export class AppStore extends TypedBaseStore { private repositoryFilterText: string = '' private currentMergeTreePromise: Promise | null = null + private currentPRMergeTreePromise: Promise | null = null /** The function to resolve the current Open in Desktop flow. */ private resolveOpenInDesktop: @@ -1480,6 +1481,40 @@ export class AppStore extends TypedBaseStore { } } + private setupMergabilityPromise( + repository: Repository, + baseBranch: Branch, + compareBranch: Branch, + onLoad: (mergeTreeResult: MergeTreeResult | null) => void, + cleanup: () => void + ) { + const mergeTreePromise = 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 + }) + .then(mergeStatus => { + this.repositoryStateCache.updateCompareState(repository, () => ({ + mergeStatus, + })) + + this.emitUpdate() + }) + .finally(() => { + this.currentMergeTreePromise = null + }) + + this.currentMergeTreePromise = mergeTreePromise + + return this.currentMergeTreePromise + } + /** This shouldn't be called directly. See `Dispatcher`. */ public _updateCompareForm( repository: Repository, @@ -7281,10 +7316,15 @@ export class AppStore extends TypedBaseStore { file: null, diff: null, }, + mergeStatus: { + kind: ComputedAction.Loading, + }, }) this.emitUpdate() + this.setupPRMergeTreePromise(repository, baseBranch, currentBranch) + if (changesetData.files.length > 0) { await this._changePullRequestFileSelection( repository, @@ -7426,6 +7466,32 @@ export class AppStore extends TypedBaseStore { this._initializePullRequestPreview(repository, baseBranch, tip.branch) } + + private setupPRMergeTreePromise( + repository: Repository, + baseBranch: Branch, + compareBranch: Branch + ) { + // Not sure why we do this..following pattern from compare branch setup + if (this.currentPRMergeTreePromise != null) { + return + } + + this.currentPRMergeTreePromise = this.setupMergabilityPromise( + repository, + baseBranch, + compareBranch, + (mergeStatus: MergeTreeResult | null) => { + this.repositoryStateCache.updatePullRequestState(repository, () => ({ + mergeStatus, + })) + this.emitUpdate() + }, + () => { + this.currentPRMergeTreePromise = null + } + ) + } } /** From 00edd315a145ff1a632a7396bd34955b9406c220 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 11:54:01 -0400 Subject: [PATCH 069/262] Create pull-request-merge-status.tsx --- .../pull-request-merge-status.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/src/ui/open-pull-request/pull-request-merge-status.tsx diff --git a/app/src/ui/open-pull-request/pull-request-merge-status.tsx b/app/src/ui/open-pull-request/pull-request-merge-status.tsx new file mode 100644 index 0000000000..b928724e04 --- /dev/null +++ b/app/src/ui/open-pull-request/pull-request-merge-status.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import { MergeResult } from '../../lib/git' + +interface IPullRequestMergeStatusProps { + /** The result of merging the pull request branch into the base branch */ + readonly mergeStatus: MergeResult +} + +/** The component to display message about the result of merging the pull + * request. */ +export class PullRequestMergeStatus extends React.Component { + public render() { + return
Merge Status
+ } +} From 20c666b1d9c2068ff95bfa500fdfa57dd517f823 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 12:45:07 -0400 Subject: [PATCH 070/262] Add merge status component to dialog --- app/src/ui/open-pull-request/open-pull-request-dialog.tsx | 3 +++ .../ui/open-pull-request/pull-request-merge-status.tsx | 6 +++--- app/styles/ui/dialogs/_open-pull-request.scss | 8 ++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index b9d09a4441..3e82257375 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -11,6 +11,7 @@ import { Octicon } from '../octicons' import * as OcticonSymbol from '../octicons/octicons.generated' import { OpenPullRequestDialogHeader } from './open-pull-request-header' import { PullRequestFilesChanged } from './pull-request-files-changed' +import { PullRequestMergeStatus } from './pull-request-merge-status' interface IOpenPullRequestDialogProps { readonly repository: Repository @@ -167,6 +168,7 @@ export class OpenPullRequestDialog extends React.Component + { public render() { - return
Merge Status
+ return
Merge Status
} } diff --git a/app/styles/ui/dialogs/_open-pull-request.scss b/app/styles/ui/dialogs/_open-pull-request.scss index 6cdbb91e7f..7534652ca0 100644 --- a/app/styles/ui/dialogs/_open-pull-request.scss +++ b/app/styles/ui/dialogs/_open-pull-request.scss @@ -31,4 +31,12 @@ justify-content: center; padding: var(--spacing-double); } + + .dialog-footer { + flex-direction: row; + } + + .pull-request-merge-status { + flex-grow: 1; + } } From 77ae9dc249baa4c249b9c5a59ef8762dd866baab Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 13:37:27 -0400 Subject: [PATCH 071/262] Make it work.. --- app/src/lib/stores/app-store.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 8c8ea53bf2..d19e9f5db4 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -1488,7 +1488,7 @@ export class AppStore extends TypedBaseStore { onLoad: (mergeTreeResult: MergeTreeResult | null) => void, cleanup: () => void ) { - const mergeTreePromise = promiseWithMinimumTimeout( + return promiseWithMinimumTimeout( () => determineMergeability(repository, baseBranch, compareBranch), 500 ) @@ -1500,19 +1500,11 @@ export class AppStore extends TypedBaseStore { return null }) .then(mergeStatus => { - this.repositoryStateCache.updateCompareState(repository, () => ({ - mergeStatus, - })) - - this.emitUpdate() + onLoad(mergeStatus) }) .finally(() => { - this.currentMergeTreePromise = null + cleanup() }) - - this.currentMergeTreePromise = mergeTreePromise - - return this.currentMergeTreePromise } /** This shouldn't be called directly. See `Dispatcher`. */ From 099c0b1bcf771e987b373acb42d26113fbe9cc2c Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 13:37:49 -0400 Subject: [PATCH 072/262] No need to check merge status if no changes --- app/src/lib/stores/app-store.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index d19e9f5db4..9a13667b9b 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7308,14 +7308,19 @@ export class AppStore extends TypedBaseStore { file: null, diff: null, }, - mergeStatus: { - kind: ComputedAction.Loading, - }, + mergeStatus: + commitSHAs.length > 0 + ? { + kind: ComputedAction.Loading, + } + : null, }) this.emitUpdate() - this.setupPRMergeTreePromise(repository, baseBranch, currentBranch) + if (commitSHAs.length > 0) { + this.setupPRMergeTreePromise(repository, baseBranch, currentBranch) + } if (changesetData.files.length > 0) { await this._changePullRequestFileSelection( From 1c7623648e647218f02f2000402ff08c5e4b9f19 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 13:38:20 -0400 Subject: [PATCH 073/262] Add messages for each merge status kind Copying dotcom verbiage --- .../pull-request-merge-status.tsx | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/app/src/ui/open-pull-request/pull-request-merge-status.tsx b/app/src/ui/open-pull-request/pull-request-merge-status.tsx index 9662ddf653..41f4ab8b90 100644 --- a/app/src/ui/open-pull-request/pull-request-merge-status.tsx +++ b/app/src/ui/open-pull-request/pull-request-merge-status.tsx @@ -1,5 +1,9 @@ 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 */ @@ -9,7 +13,56 @@ interface IPullRequestMergeStatusProps { /** The component to display message about the result of merging the pull * request. */ export class PullRequestMergeStatus extends React.Component { + private getMergeStatusDescription = () => { + const { mergeStatus } = this.props + if (mergeStatus === null) { + return '' + } + + const { kind } = mergeStatus + switch (kind) { + case ComputedAction.Loading: + return ( + + Checking mergeability… Don’t worry, you can + still create the pull request. + + ) + case ComputedAction.Invalid: + return ( + + Error checking merge status. Don’t worry, you can + still create the pull request. + + ) + case ComputedAction.Clean: + return ( + + + Able to merge. + {' '} + These branches can be automatically merged. + + ) + case ComputedAction.Conflicts: + return ( + + + Can't automatically merge. + {' '} + Don’t worry, you can still create the pull request. + + ) + default: + return assertNever(kind, `Unknown merge status kind of ${kind}.`) + } + } + public render() { - return
Merge Status
+ return ( +
+ {this.getMergeStatusDescription()} +
+ ) } } From fca7928f7324c5db5ff2c61e63fd9fa877d28d96 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 14:02:28 -0400 Subject: [PATCH 074/262] Style the messages --- app/styles/_ui.scss | 1 + app/styles/ui/_pull-request-merge-status.scss | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 app/styles/ui/_pull-request-merge-status.scss diff --git a/app/styles/_ui.scss b/app/styles/_ui.scss index 53c78ec973..1122e15c7d 100644 --- a/app/styles/_ui.scss +++ b/app/styles/_ui.scss @@ -102,3 +102,4 @@ @import 'ui/_branch-select'; @import 'ui/_popover-dropdown'; @import 'ui/_pull-request-files-changed'; +@import 'ui/_pull-request-merge-status'; diff --git a/app/styles/ui/_pull-request-merge-status.scss b/app/styles/ui/_pull-request-merge-status.scss new file mode 100644 index 0000000000..09cb1edb1c --- /dev/null +++ b/app/styles/ui/_pull-request-merge-status.scss @@ -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(--warning-badge-icon-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); + } + } +} From c4e751f6e17da3daac02fee63439ed1095b9bbcd Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 14:13:26 -0400 Subject: [PATCH 075/262] Reuse new method for existing logic --- app/src/lib/stores/app-store.ts | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 9a13667b9b..2fdf777074 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -1442,34 +1442,21 @@ export class AppStore extends TypedBaseStore { } if (tip.kind === TipState.Valid && aheadBehind.behind > 0) { - const mergeTreePromise = promiseWithMinimumTimeout( - () => determineMergeability(repository, tip.branch, action.branch), - 500 - ) - .catch(err => { - log.warn( - `Error occurred while trying to merge ${tip.branch.name} (${tip.branch.tip.sha}) and ${action.branch.name} (${action.branch.tip.sha})`, - err - ) - return null - }) - .then(mergeStatus => { + this.currentMergeTreePromise = this.setupMergabilityPromise( + repository, + tip.branch, + action.branch, + mergeStatus => { this.repositoryStateCache.updateCompareState(repository, () => ({ mergeStatus, })) this.emitUpdate() - }) - - const cleanup = () => { - this.currentMergeTreePromise = null - } - - // TODO: when we have Promise.prototype.finally available we - // should use that here to make this intent clearer - mergeTreePromise.then(cleanup, cleanup) - - this.currentMergeTreePromise = mergeTreePromise + }, + () => { + this.currentMergeTreePromise = null + } + ) return this.currentMergeTreePromise } else { From 5a3056fc720e5e5d3391589a4e4460a998deee21 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 14:18:22 -0400 Subject: [PATCH 076/262] Remove copied stuff non applicable to my use case --- app/src/lib/stores/app-store.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 2fdf777074..66a52a425d 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -473,7 +473,6 @@ export class AppStore extends TypedBaseStore { private repositoryFilterText: string = '' private currentMergeTreePromise: Promise | null = null - private currentPRMergeTreePromise: Promise | null = null /** The function to resolve the current Open in Desktop flow. */ private resolveOpenInDesktop: @@ -1473,7 +1472,7 @@ export class AppStore extends TypedBaseStore { baseBranch: Branch, compareBranch: Branch, onLoad: (mergeTreeResult: MergeTreeResult | null) => void, - cleanup: () => void + cleanup?: () => void ) { return promiseWithMinimumTimeout( () => determineMergeability(repository, baseBranch, compareBranch), @@ -1490,7 +1489,7 @@ export class AppStore extends TypedBaseStore { onLoad(mergeStatus) }) .finally(() => { - cleanup() + cleanup?.() }) } @@ -7456,12 +7455,7 @@ export class AppStore extends TypedBaseStore { baseBranch: Branch, compareBranch: Branch ) { - // Not sure why we do this..following pattern from compare branch setup - if (this.currentPRMergeTreePromise != null) { - return - } - - this.currentPRMergeTreePromise = this.setupMergabilityPromise( + this.setupMergabilityPromise( repository, baseBranch, compareBranch, @@ -7470,9 +7464,6 @@ export class AppStore extends TypedBaseStore { mergeStatus, })) this.emitUpdate() - }, - () => { - this.currentPRMergeTreePromise = null } ) } From d8bc8464f9521d7ae5726a4daf5590a9f6c2c1b9 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 4 Oct 2022 14:25:25 -0400 Subject: [PATCH 077/262] More legible warning color --- app/styles/ui/_pull-request-merge-status.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/ui/_pull-request-merge-status.scss b/app/styles/ui/_pull-request-merge-status.scss index 09cb1edb1c..f9d3377ae6 100644 --- a/app/styles/ui/_pull-request-merge-status.scss +++ b/app/styles/ui/_pull-request-merge-status.scss @@ -12,7 +12,7 @@ .pr-merge-status-loading { strong { - color: var(--warning-badge-icon-color); + color: var(--file-warning-color); } } From 596aa1a991bc3cc73624c75559ec49480aa0c9a2 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 6 Oct 2022 08:31:56 -0400 Subject: [PATCH 078/262] Use a promise like your supposed to.. --- app/src/lib/stores/app-store.ts | 42 ++++++++++++--------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 66a52a425d..8e6a9cf361 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -1444,18 +1444,18 @@ export class AppStore extends TypedBaseStore { this.currentMergeTreePromise = this.setupMergabilityPromise( repository, tip.branch, - action.branch, - mergeStatus => { + action.branch + ) + .then(mergeStatus => { this.repositoryStateCache.updateCompareState(repository, () => ({ mergeStatus, })) this.emitUpdate() - }, - () => { + }) + .finally(() => { this.currentMergeTreePromise = null - } - ) + }) return this.currentMergeTreePromise } else { @@ -1470,27 +1470,18 @@ export class AppStore extends TypedBaseStore { private setupMergabilityPromise( repository: Repository, baseBranch: Branch, - compareBranch: Branch, - onLoad: (mergeTreeResult: MergeTreeResult | null) => void, - cleanup?: () => void + 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 - }) - .then(mergeStatus => { - onLoad(mergeStatus) - }) - .finally(() => { - cleanup?.() - }) + ).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`. */ @@ -7455,10 +7446,7 @@ export class AppStore extends TypedBaseStore { baseBranch: Branch, compareBranch: Branch ) { - this.setupMergabilityPromise( - repository, - baseBranch, - compareBranch, + this.setupMergabilityPromise(repository, baseBranch, compareBranch).then( (mergeStatus: MergeTreeResult | null) => { this.repositoryStateCache.updatePullRequestState(repository, () => ({ mergeStatus, From 6a4a279c7d6d36ca22d7c01d708f5a797d96cf04 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 6 Oct 2022 09:03:15 -0400 Subject: [PATCH 079/262] Handle no merge base --- app/src/lib/git/diff.ts | 20 ++++++++++++-------- app/src/lib/stores/app-store.ts | 4 ++-- app/test/unit/git/diff-test.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/app/src/lib/git/diff.ts b/app/src/lib/git/diff.ts index 8878a0af03..0f7f0fcb4d 100644 --- a/app/src/lib/git/diff.ts +++ b/app/src/lib/git/diff.ts @@ -254,7 +254,7 @@ export async function getBranchMergeBaseChangedFiles( baseBranchName: string, comparisonBranchName: string, latestComparisonBranchCommitRef: string -): Promise { +): Promise { const baseArgs = [ 'diff', '--merge-base', @@ -268,22 +268,26 @@ export async function getBranchMergeBaseChangedFiles( '--', ] - const result = await git( - baseArgs, - repository.path, - 'getBranchMergeBaseChangedFiles' - ) - const mergeBaseCommit = await getMergeBase( repository, baseBranchName, comparisonBranchName ) + if (mergeBaseCommit === null) { + return null + } + + const result = await git( + baseArgs, + repository.path, + 'getBranchMergeBaseChangedFiles' + ) + return parseRawLogWithNumstat( result.combinedOutput, `${latestComparisonBranchCommitRef}`, - mergeBaseCommit ?? NullTreeSHA + mergeBaseCommit ) } diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 8e6a9cf361..04888116ef 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7281,7 +7281,7 @@ export class AppStore extends TypedBaseStore { shas: commitSHAs, shasInDiff: commitSHAs, isContiguous: true, - changesetData, + changesetData: changesetData !== null ? changesetData : emptyChangeSet, file: null, diff: null, }, @@ -7299,7 +7299,7 @@ export class AppStore extends TypedBaseStore { this.setupPRMergeTreePromise(repository, baseBranch, currentBranch) } - if (changesetData.files.length > 0) { + if (changesetData !== null && changesetData.files.length > 0) { await this._changePullRequestFileSelection( repository, changesetData.files[0] diff --git a/app/test/unit/git/diff-test.ts b/app/test/unit/git/diff-test.ts index 4ba2f5d8fd..8e1a2f198d 100644 --- a/app/test/unit/git/diff-test.ts +++ b/app/test/unit/git/diff-test.ts @@ -575,9 +575,38 @@ describe('git/diff', () => { 'feature-branch', 'irrelevantToTest' ) + + expect(changesetData).not.toBeNull() + if (changesetData === null) { + return + } + expect(changesetData.files).toHaveLength(1) expect(changesetData.files[0].path).toBe('feature.md') }) + + it('returns null for unrelated histories', async () => { + // create a second branch that's orphaned from our current branch + await GitProcess.exec( + ['checkout', '--orphan', 'orphaned-branch'], + repository.path + ) + + // add a commit to this new branch + await GitProcess.exec( + ['commit', '--allow-empty', '-m', `first commit on gh-pages`], + repository.path + ) + + const changesetData = await getBranchMergeBaseChangedFiles( + repository, + 'master', + 'feature-branch', + 'irrelevantToTest' + ) + + expect(changesetData).toBeNull() + }) }) describe('getBranchMergeBaseDiff', () => { From fbd5def4a345057e0c3a454d8b2978c2750a7823 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 6 Oct 2022 09:05:08 -0400 Subject: [PATCH 080/262] Update invalid to represent unrelated histories --- app/src/ui/open-pull-request/pull-request-merge-status.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/ui/open-pull-request/pull-request-merge-status.tsx b/app/src/ui/open-pull-request/pull-request-merge-status.tsx index 41f4ab8b90..beebe4ddfe 100644 --- a/app/src/ui/open-pull-request/pull-request-merge-status.tsx +++ b/app/src/ui/open-pull-request/pull-request-merge-status.tsx @@ -31,8 +31,8 @@ export class PullRequestMergeStatus extends React.Component - Error checking merge status. Don’t worry, you can - still create the pull request. + Error checking merge status. Unable to merge + unrelated histories in this repository ) case ComputedAction.Clean: From 7d1ecec457cf1ed06bcd2ae17da8e5950705f05e Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 6 Oct 2022 09:21:08 -0400 Subject: [PATCH 081/262] Don't bother with mergeability check if no mergebase --- app/src/lib/stores/app-store.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 04888116ef..afed3ee5c9 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7254,18 +7254,18 @@ export class AppStore extends TypedBaseStore { currentBranch ) - const commitSHAs = pullRequestCommits.map(c => c.sha) + const commitsBetweenBranches = pullRequestCommits.map(c => c.sha) // A user may compare two branches with no changes between them. const emptyChangeSet = { files: [], linesAdded: 0, linesDeleted: 0 } const changesetData = - commitSHAs.length > 0 + commitsBetweenBranches.length > 0 ? await gitStore.performFailableOperation(() => getBranchMergeBaseChangedFiles( repository, baseBranch.name, currentBranch.name, - commitSHAs[0] + commitsBetweenBranches[0] ) ) : emptyChangeSet @@ -7274,6 +7274,11 @@ export class AppStore extends TypedBaseStore { return } + const hasMergeBase = changesetData !== null + // We don't care how many commits exist on the unrelated history that + // can't be merged. + const commitSHAs = hasMergeBase ? commitsBetweenBranches : [] + this.repositoryStateCache.initializePullRequestState(repository, { baseBranch, commitSHAs, @@ -7281,14 +7286,16 @@ export class AppStore extends TypedBaseStore { shas: commitSHAs, shasInDiff: commitSHAs, isContiguous: true, - changesetData: changesetData !== null ? changesetData : emptyChangeSet, + changesetData: changesetData ?? emptyChangeSet, file: null, diff: null, }, mergeStatus: - commitSHAs.length > 0 + commitSHAs.length > 0 || !hasMergeBase ? { - kind: ComputedAction.Loading, + kind: hasMergeBase + ? ComputedAction.Loading + : ComputedAction.Invalid, } : null, }) From 55f4d561d5b56800127e182fb28e10dd749ddf30 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 6 Oct 2022 09:21:23 -0400 Subject: [PATCH 082/262] Disable create pull request button for no changes --- app/src/ui/open-pull-request/open-pull-request-dialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 3e82257375..10f0c7856a 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -168,7 +168,7 @@ export class OpenPullRequestDialog extends React.Component
) From c2232a52c31153bc6870c4cec22e67a2aaa15e1c Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 6 Oct 2022 09:26:56 -0400 Subject: [PATCH 083/262] Add message about unrelated commit histories to no changes view --- .../open-pull-request-dialog.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 10f0c7856a..4a470907dd 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -12,6 +12,7 @@ import * as OcticonSymbol from '../octicons/octicons.generated' import { OpenPullRequestDialogHeader } from './open-pull-request-header' import { PullRequestFilesChanged } from './pull-request-files-changed' import { PullRequestMergeStatus } from './pull-request-merge-status' +import { ComputedAction } from '../../models/computed-action' interface IOpenPullRequestDialogProps { readonly repository: Repository @@ -148,20 +149,30 @@ export class OpenPullRequestDialog extends React.Component + {baseBranch.name} is up to date with all commits from{' '} + {currentBranch.name}. + + ) : ( + <> + {baseBranch.name} and {currentBranch.name} are + entirely different commit histories. + + ) return (

There are no changes.

- {baseBranch.name} is up to date with all commits from{' '} - {currentBranch.name}. + {message}
) From 6eefbc96c95426fdf7d7cf156d8a90f25799c31f Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 6 Oct 2022 10:56:42 -0400 Subject: [PATCH 084/262] Release 3.1.2-beta1 --- app/package.json | 2 +- app/src/lib/feature-flag.ts | 2 +- changelog.json | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/package.json b/app/package.json index 17b1c3d945..9273708c62 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "productName": "GitHub Desktop", "bundleID": "com.github.GitHubClient", "companyName": "GitHub, Inc.", - "version": "3.1.1", + "version": "3.1.2-beta1", "main": "./main.js", "repository": { "type": "git", diff --git a/app/src/lib/feature-flag.ts b/app/src/lib/feature-flag.ts index dccc1a383d..f411e68b1a 100644 --- a/app/src/lib/feature-flag.ts +++ b/app/src/lib/feature-flag.ts @@ -110,5 +110,5 @@ export function enableSubmoduleDiff(): boolean { /** Should we enable starting pull requests? */ export function enableStartingPullRequests(): boolean { - return enableDevelopmentFeatures() + return enableBetaFeatures() } diff --git a/changelog.json b/changelog.json index fd84fb588e..8333912851 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,11 @@ { "releases": { + "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] Added support zooming out for the 67%, 75%, 80% and 90% zoom levels - #15401. Thanks @sathvikrijo!", + "[Improved] Add option to disable discard stash confirmation - #15379. Thanks @tsvetilian-ty!" + ], "3.1.1": [ "[Fixed] App correctly remembers undo commit prompt setting - #15408" ], From bccec59c0849896978f578f7e95d5578e6bedb14 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 6 Oct 2022 10:58:28 -0400 Subject: [PATCH 085/262] Update changelog.json --- changelog.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.json b/changelog.json index 8333912851..4a57665760 100644 --- a/changelog.json +++ b/changelog.json @@ -3,7 +3,7 @@ "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] Added support zooming out for the 67%, 75%, 80% and 90% zoom levels - #15401. Thanks @sathvikrijo!", + "[Improved] Added 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": [ From 397a12b5786b7fed522ede9103285605a76404c5 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Mon, 10 Oct 2022 10:40:23 +0200 Subject: [PATCH 086/262] Update changelog.json --- changelog.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.json b/changelog.json index 4a57665760..f53274b243 100644 --- a/changelog.json +++ b/changelog.json @@ -3,7 +3,7 @@ "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] Added support for zooming out at the 67%, 75%, 80% and 90% zoom levels - #15401. Thanks @sathvikrijo!", + "[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": [ From 1a1abaede57556be92b44c1a5a4171c8735c730d Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Mon, 10 Oct 2022 14:05:29 +0200 Subject: [PATCH 087/262] Use "loading" indicator from header --- app/src/ui/installing-update/installing-update.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index 308381cabc..c24afcb9ef 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -9,7 +9,6 @@ import { } from '../dialog' import { updateStore, IUpdateState, UpdateStatus } from '../lib/update-store' import { Disposable } from 'event-kit' -import { Loading } from '../lib/loading' import { DialogHeader } from '../dialog/header' import { Dispatcher } from '../dispatcher' @@ -76,15 +75,14 @@ export class InstallingUpdate extends React.Component { > - - - Please, do not close GitHub Desktop until the update is completely - installed. - + Please, do not close GitHub Desktop until the update is completely + installed. From c334c203c83fa116fe7c200b70cc45a480053c9d Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Mon, 10 Oct 2022 16:18:27 +0200 Subject: [PATCH 088/262] Revert changes we don't want merged --- app/src/main-process/app-window.ts | 3 +-- app/src/ui/lib/update-store.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main-process/app-window.ts b/app/src/main-process/app-window.ts index 369a1039e8..7c1af9e7c9 100644 --- a/app/src/main-process/app-window.ts +++ b/app/src/main-process/app-window.ts @@ -116,8 +116,7 @@ export class AppWindow { if ( (!__DARWIN__ || quitting) && !quittingEvenIfUpdating && - // TODO: DON'T MERGE THIS 1!== NaN!!!! - (1 !== NaN || this.isDownloadingUpdate) + this.isDownloadingUpdate ) { e.preventDefault() ipcWebContents.send(this.window.webContents, 'show-installing-update') diff --git a/app/src/ui/lib/update-store.ts b/app/src/ui/lib/update-store.ts index 03d93b4aad..e4ad40d9ae 100644 --- a/app/src/ui/lib/update-store.ts +++ b/app/src/ui/lib/update-store.ts @@ -56,7 +56,7 @@ export interface IUpdateState { /** A store which contains the current state of the auto updater. */ class UpdateStore { private emitter = new Emitter() - private status = UpdateStatus.UpdateAvailable // // TODO: DON'T MERGE THIS, IT SHOULD BE UpdateStatus.UpdateNotChecked + private status = UpdateStatus.UpdateNotChecked private lastSuccessfulCheck: Date | null = null private newReleases: ReadonlyArray | null = null private isX64ToARM64ImmediateAutoUpdate: boolean = false From 4dbcf07b3817dd31fb6ef92a10aa3b1dc3b98d58 Mon Sep 17 00:00:00 2001 From: Niko-O Date: Mon, 10 Oct 2022 16:26:45 +0200 Subject: [PATCH 089/262] Add link to build instructions to README.md Those instructions are otherwise buried behind 2 links. That is, I first have to see the link to CONTRIBUTING.md, then find the tiny "Set Up Your Machine" section in there. In my case, I wasn't even going at it from the perspective of trying to contribute; I just wanted to build the program. So I had a very hard time finding this information. Adding this link here would make finding this infromation a lot easier. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 663a8dfedf..6a2a683452 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,10 @@ resources relevant to the project. If you're looking for something to work on, check out the [help wanted](https://github.com/desktop/desktop/issues?q=is%3Aissue+is%3Aopen+label%3A%22help%20wanted%22) label. +## Building Desktop + +To get your development environment set up for building Desktop, see [setup.md](./docs/contributing/setup.md). + ## More Resources See [desktop.github.com](https://desktop.github.com) for more product-oriented From 8c100bb0c75a52ae1ee9d7feb36fa594ff2eaadd Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 11 Oct 2022 13:29:10 +0200 Subject: [PATCH 090/262] Make the alert "scarier" --- app/src/ui/installing-update/installing-update.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index c24afcb9ef..220ec8dfd8 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -72,6 +72,7 @@ export class InstallingUpdate extends React.Component { id="installing-update" onSubmit={this.props.onDismissed} dismissable={false} + type="warning" > { Please, do not close GitHub Desktop until the update is completely - installed. + installed. Quitting the app now may result in a corrupted + installation. @@ -90,6 +92,7 @@ export class InstallingUpdate extends React.Component { okButtonText={__DARWIN__ ? 'Quit Anyway' : 'Quit anyway'} onOkButtonClick={this.onQuitAnywayButtonClicked} onCancelButtonClick={this.onCancel} + destructive={true} /> From 58554a6182b6c1490163bb25f257741a1d57ee4d Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 11 Oct 2022 13:45:25 +0200 Subject: [PATCH 091/262] Improve verbiage Co-Authored-By: Maximilian Sachs <15086212+masachs@users.noreply.github.com> --- app/src/ui/installing-update/installing-update.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index 220ec8dfd8..239d04c85b 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -82,9 +82,8 @@ export class InstallingUpdate extends React.Component { /> - Please, do not close GitHub Desktop until the update is completely - installed. Quitting the app now may result in a corrupted - installation. + Do not close GitHub Desktop while the update is in progress. Closing + now may break your installation. From 4e3ff0a7c88ad64b15930a3203c73124ba44b051 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 11 Oct 2022 17:10:36 +0200 Subject: [PATCH 092/262] Calculate the gutter width for unified diffs properly --- app/src/ui/diff/side-by-side-diff-row.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/ui/diff/side-by-side-diff-row.tsx b/app/src/ui/diff/side-by-side-diff-row.tsx index 1223f7215c..8aa385b462 100644 --- a/app/src/ui/diff/side-by-side-diff-row.tsx +++ b/app/src/ui/diff/side-by-side-diff-row.tsx @@ -363,6 +363,16 @@ export class SideBySideDiffRow extends React.Component< throw new Error(`Unexpected expansion type ${expansionType}`) } + /** + * This method returns the width of a line gutter in pixels. For unified diffs + * the gutter contains the line number of both before and after sides, whereas + * for side-by-side diffs the gutter contains the line number of only one side. + */ + private get lineGutterWidth() { + const { showSideBySideDiff, lineNumberWidth } = this.props + return showSideBySideDiff ? lineNumberWidth : lineNumberWidth * 2 + } + private renderHunkExpansionHandle( hunkIndex: number, expansionType: DiffHunkExpansionType @@ -372,7 +382,7 @@ export class SideBySideDiffRow extends React.Component<
@@ -389,7 +399,7 @@ export class SideBySideDiffRow extends React.Component<
+
{lineNumbers.map((lineNumber, index) => ( {lineNumber} ))} @@ -470,7 +477,7 @@ export class SideBySideDiffRow extends React.Component< 'line-selected': isSelected, hover: this.props.isHunkHovered, })} - style={{ width: this.props.lineNumberWidth }} + style={{ width: this.lineGutterWidth }} onMouseDown={this.onMouseDownLineNumber} onContextMenu={this.onContextMenuLineNumber} > @@ -493,7 +500,7 @@ export class SideBySideDiffRow extends React.Component< const style: React.CSSProperties = { [column === DiffColumn.Before ? 'marginRight' : 'marginLeft']: - this.props.lineNumberWidth + 10, + this.lineGutterWidth + 10, marginTop: -10, } From b1b0a6f25c707c0f068e47169c588c342b7f21a8 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 11 Oct 2022 17:10:56 +0200 Subject: [PATCH 093/262] Fix colors of hunk handle in unified diff --- app/styles/ui/_side-by-side-diff.scss | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/styles/ui/_side-by-side-diff.scss b/app/styles/ui/_side-by-side-diff.scss index b16bcb85c2..5d86956932 100644 --- a/app/styles/ui/_side-by-side-diff.scss +++ b/app/styles/ui/_side-by-side-diff.scss @@ -322,13 +322,22 @@ flex-direction: row; } + .hunk-handle { left: 100px; } - &.hunk-info .line-number { - background: var(--diff-hunk-gutter-background-color); - border-color: var(--diff-hunk-border-color); + &.hunk-info { + .line-number { + background: var(--diff-hunk-gutter-background-color); + border-color: var(--diff-hunk-border-color); + } + .hunk-expansion-handle { + background: var(--diff-hunk-gutter-background-color); + border-right-width: 4px; + border-right-style: solid; + border-color: var(--diff-hunk-gutter-background-color); + } } .line-number { From f2d7d3281d25ac6fbd7d428894aa427867600547 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 11 Oct 2022 17:11:06 +0200 Subject: [PATCH 094/262] Enable experimental diff viewer for unified diffs --- app/src/lib/feature-flag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/feature-flag.ts b/app/src/lib/feature-flag.ts index f411e68b1a..d5addc68fe 100644 --- a/app/src/lib/feature-flag.ts +++ b/app/src/lib/feature-flag.ts @@ -46,7 +46,7 @@ export function enableWSLDetection(): boolean { * Should we use the new diff viewer for unified diffs? */ export function enableExperimentalDiffViewer(): boolean { - return false + return enableBetaFeatures() } /** From 2411ec53a75d1741b6e85a3163a9365849d59d8c Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 11 Oct 2022 18:05:19 +0200 Subject: [PATCH 095/262] Fix appearance of experimental diff viewer in unified mode --- app/styles/ui/_side-by-side-diff.scss | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/styles/ui/_side-by-side-diff.scss b/app/styles/ui/_side-by-side-diff.scss index 5d86956932..38c3de2137 100644 --- a/app/styles/ui/_side-by-side-diff.scss +++ b/app/styles/ui/_side-by-side-diff.scss @@ -334,9 +334,19 @@ } .hunk-expansion-handle { background: var(--diff-hunk-gutter-background-color); - border-right-width: 4px; + border-right-width: 1px; border-right-style: solid; border-color: var(--diff-hunk-gutter-background-color); + &.selectable:hover { + border-color: var(--diff-hover-background-color); + } + &:not(.selectable) span { + // This must be the height of an octicon, so that when a hunk handle + // of any type is replaced by an "empty" hunk handle (e.g. when a + // file is completely expanded), the height of the whole line + // remains the same. + height: 16px; + } } } @@ -358,8 +368,13 @@ } } - &.editable .row .line-number { - border-right-width: 4px; + &.editable .row { + .line-number { + border-right-width: 4px; + } + .hunk-expansion-handle { + border-right-width: 4px; + } } } From 379b7d104558c9bd0005ead368e32d0bc8d252f1 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 11 Oct 2022 18:14:48 +0200 Subject: [PATCH 096/262] Fix copying all content in unified diff of experimental diff viewer --- app/src/ui/diff/side-by-side-diff.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/diff/side-by-side-diff.tsx b/app/src/ui/diff/side-by-side-diff.tsx index b3d026c12f..94b594dc0b 100644 --- a/app/src/ui/diff/side-by-side-diff.tsx +++ b/app/src/ui/diff/side-by-side-diff.tsx @@ -256,7 +256,7 @@ export class SideBySideDiff extends React.Component< : [DiffLineType.Add, DiffLineType.Context] : [DiffLineType.Add, DiffLineType.Delete, DiffLineType.Context] - const contents = this.props.diff.hunks + const contents = this.state.diff.hunks .flatMap(h => h.lines .filter(line => lineTypes.includes(line.type)) From f3e32e8e59f85e0704a43730f84d957b5f6ed057 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 11 Oct 2022 18:21:18 +0200 Subject: [PATCH 097/262] Fix cmd/control + Enter shortcut while amending a commit --- app/src/ui/changes/commit-message.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/changes/commit-message.tsx b/app/src/ui/changes/commit-message.tsx index 24c3355a1c..e3a9c38899 100644 --- a/app/src/ui/changes/commit-message.tsx +++ b/app/src/ui/changes/commit-message.tsx @@ -351,7 +351,7 @@ export class CommitMessage extends React.Component< if ( isShortcutKey && event.key === 'Enter' && - this.canCommit() && + (this.canCommit() || this.canAmend()) && this.canExcecuteCommitShortcut() ) { this.createCommit() From 8b827a336e3a0e6ae67d8585f145199441d8b757 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 12 Oct 2022 10:23:52 +0200 Subject: [PATCH 098/262] =?UTF-8?q?Fix=20linting=20issue=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/styles/ui/_side-by-side-diff.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/styles/ui/_side-by-side-diff.scss b/app/styles/ui/_side-by-side-diff.scss index 38c3de2137..c512cb2045 100644 --- a/app/styles/ui/_side-by-side-diff.scss +++ b/app/styles/ui/_side-by-side-diff.scss @@ -322,7 +322,6 @@ flex-direction: row; } - .hunk-handle { left: 100px; } From 581567d64b2f847b33a0d22b55986fda72210a17 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 12 Oct 2022 10:45:56 +0200 Subject: [PATCH 099/262] Make sure Squirrel.Windows isn't affected by partially or completely disabled releases --- script/package.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/script/package.ts b/script/package.ts index 7f8566adb2..8cfd8f03c7 100644 --- a/script/package.ts +++ b/script/package.ts @@ -102,7 +102,11 @@ function packageWindows() { } if (shouldMakeDelta()) { - options.remoteReleases = getUpdatesURL() + const url = new URL(getUpdatesURL()) + // Make sure Squirrel.Windows isn't affected by partially or completely + // disabled releases. + url.searchParams.set('bypassStaggeredRelease', '1') + options.remoteReleases = url.toString() } if (isAppveyor() || isGitHubActions()) { From 8675ea638b54b95784067858e3cf2aa60823634d Mon Sep 17 00:00:00 2001 From: Angus Date: Sat, 15 Oct 2022 01:49:44 +0100 Subject: [PATCH 100/262] Fix avatar tooltip does not wrap properly in some cases --- app/styles/ui/window/_tooltips.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/ui/window/_tooltips.scss b/app/styles/ui/window/_tooltips.scss index 97282eda85..27f84b7439 100644 --- a/app/styles/ui/window/_tooltips.scss +++ b/app/styles/ui/window/_tooltips.scss @@ -11,6 +11,7 @@ body > .tooltip, max-width: 300px; word-wrap: break-word; + word-break: break-word; overflow-wrap: break-word; background-color: var(--tooltip-background-color); From 56e965cb10142dbbe06e2f1e1914ba22493f3f99 Mon Sep 17 00:00:00 2001 From: Angus Date: Sat, 15 Oct 2022 20:35:02 +0100 Subject: [PATCH 101/262] Fix issue #13636 - Tooltip position is (0,0) if mouse is not moved The tooltip position is referenced to mouse position set in mousemove event. If the tooltip is triggered by events other than mousemove (e.g. scroll the list with mousewheel or keyboard arrow keys, or click the Fetch Origin button without moving the mouse), the mouse position is not set (0,0). The fix is to set the mouse position in mouseenter event as well --- app/src/ui/lib/tooltip.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/ui/lib/tooltip.tsx b/app/src/ui/lib/tooltip.tsx index 6e4b05abbf..5d47f02ae4 100644 --- a/app/src/ui/lib/tooltip.tsx +++ b/app/src/ui/lib/tooltip.tsx @@ -300,6 +300,8 @@ export class Tooltip extends React.Component< } private onTargetMouseEnter = (event: MouseEvent) => { + this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20) + this.mouseOverTarget = true this.cancelHideTooltip() if (!this.state.show) { From 9ffb2041679feafd668f536ccaa4f745b4a70330 Mon Sep 17 00:00:00 2001 From: Angus Date: Sun, 16 Oct 2022 00:35:25 +0100 Subject: [PATCH 102/262] Issue 11953 Progress bar during cloning partially hidden Close the repository list when clone starts --- app/src/ui/clone-repository/clone-repository.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/ui/clone-repository/clone-repository.tsx b/app/src/ui/clone-repository/clone-repository.tsx index d89646a093..cfca9408fc 100644 --- a/app/src/ui/clone-repository/clone-repository.tsx +++ b/app/src/ui/clone-repository/clone-repository.tsx @@ -3,6 +3,7 @@ import * as React from 'react' import { Dispatcher } from '../dispatcher' import { getDefaultDir, setDefaultDir } from '../lib/default-dir' import { Account } from '../../models/account' +import { FoldoutType } from '../../lib/app-state' import { IRepositoryIdentifier, parseRepositoryIdentifier, @@ -728,6 +729,7 @@ export class CloneRepository extends React.Component< const { url, defaultBranch } = cloneInfo + this.props.dispatcher.closeFoldout(FoldoutType.Repository) try { this.cloneImpl(url.trim(), path, defaultBranch) } catch (e) { From aac5fd4d435081864e2794fe2f7d32c59b257473 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 17 Oct 2022 08:16:22 -0400 Subject: [PATCH 103/262] Consistently disable multi commit operations --- app/src/ui/history/commit-list-item.tsx | 33 +++++++++++++++++++------ app/src/ui/history/commit-list.tsx | 24 +++++++++++++----- app/src/ui/history/compare.tsx | 6 +++-- app/src/ui/repository.tsx | 7 +----- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/app/src/ui/history/commit-list-item.tsx b/app/src/ui/history/commit-list-item.tsx index 998342bbe5..dc4d4bb4be 100644 --- a/app/src/ui/history/commit-list-item.tsx +++ b/app/src/ui/history/commit-list-item.tsx @@ -51,8 +51,8 @@ interface ICommitProps { readonly showUnpushedIndicator: boolean readonly unpushedIndicatorTitle?: string readonly unpushedTags?: ReadonlyArray - readonly isCherryPickInProgress?: boolean readonly disableSquashing?: boolean + readonly isMultiCommitOperationInProgress?: boolean } interface ICommitListItemState { @@ -126,7 +126,7 @@ export class CommitListItem extends React.PureComponent< author: { date }, } = commit - const isDraggable = this.canCherryPick() + const isDraggable = this.isDraggable() const hasEmptySummary = commit.summary.length === 0 const commitSummary = hasEmptySummary ? 'Empty commit message' @@ -379,17 +379,34 @@ export class CommitListItem extends React.PureComponent< ? `Squash ${count} Commits…` : `Squash ${count} commits…`, action: this.onSquash, + enabled: this.canSquash(), }, ] } - private canCherryPick(): boolean { - const { onCherryPick, isCherryPickInProgress } = this.props + private isDraggable(): boolean { + const { onCherryPick, onSquash, isMultiCommitOperationInProgress } = + this.props return ( - onCherryPick !== undefined && - this.onSquash !== undefined && - isCherryPickInProgress === false - // TODO: isSquashInProgress === false + (onCherryPick !== undefined || onSquash !== undefined) && + isMultiCommitOperationInProgress === false + ) + } + + private canCherryPick(): boolean { + const { onCherryPick, isMultiCommitOperationInProgress } = this.props + return ( + onCherryPick !== undefined && isMultiCommitOperationInProgress === false + ) + } + + private canSquash(): boolean { + const { onSquash, disableSquashing, isMultiCommitOperationInProgress } = + this.props + return ( + onSquash !== undefined && + disableSquashing === false && + isMultiCommitOperationInProgress === false ) } diff --git a/app/src/ui/history/commit-list.tsx b/app/src/ui/history/commit-list.tsx index d176775bac..9b4734b269 100644 --- a/app/src/ui/history/commit-list.tsx +++ b/app/src/ui/history/commit-list.tsx @@ -119,8 +119,9 @@ interface ICommitListProps { /** Whether or not commits in this list can be reordered. */ readonly reorderingEnabled?: boolean - /** Whether a cherry pick is progress */ - readonly isCherryPickInProgress?: boolean + /** Whether a multi commit operation is in progress (in particular the + * conflicts resolution step allows interaction with history) */ + readonly isMultiCommitOperationInProgress?: boolean /** Callback to render commit drag element */ readonly onRenderCommitDragElement?: ( @@ -213,10 +214,12 @@ export class CommitList extends React.Component { onAmendCommit={this.props.onAmendCommit} onViewCommitOnGitHub={this.props.onViewCommitOnGitHub} selectedCommits={this.lookupCommits(this.props.selectedSHAs)} - isCherryPickInProgress={this.props.isCherryPickInProgress} onRenderCommitDragElement={this.onRenderCommitDragElement} onRemoveDragElement={this.props.onRemoveCommitDragElement} disableSquashing={this.props.disableSquashing} + isMultiCommitOperationInProgress={ + this.props.isMultiCommitOperationInProgress + } /> ) } @@ -374,8 +377,14 @@ export class CommitList extends React.Component { } public render() { - const { commitSHAs, selectedSHAs, shasToHighlight, emptyListMessage } = - this.props + const { + commitSHAs, + selectedSHAs, + shasToHighlight, + emptyListMessage, + reorderingEnabled, + isMultiCommitOperationInProgress, + } = this.props if (commitSHAs.length === 0) { return (
@@ -402,7 +411,10 @@ export class CommitList extends React.Component { selectionMode="multi" onScroll={this.onScroll} insertionDragType={ - this.props.reorderingEnabled === true ? DragType.Commit : undefined + reorderingEnabled === true && + isMultiCommitOperationInProgress === false + ? DragType.Commit + : undefined } invalidationProps={{ commits: this.props.commitSHAs, diff --git a/app/src/ui/history/compare.tsx b/app/src/ui/history/compare.tsx index e77438ecda..ad5f8598f6 100644 --- a/app/src/ui/history/compare.tsx +++ b/app/src/ui/history/compare.tsx @@ -53,7 +53,7 @@ interface ICompareSidebarProps { readonly localTags: Map | null readonly tagsToPush: ReadonlyArray | null readonly aheadBehindStore: AheadBehindStore - readonly isCherryPickInProgress: boolean + readonly isMultiCommitOperationInProgress?: boolean readonly shasToHighlight: ReadonlyArray } @@ -258,10 +258,12 @@ export class CompareSidebar extends React.Component< onCompareListScrolled={this.props.onCompareListScrolled} compareListScrollTop={this.props.compareListScrollTop} tagsToPush={this.props.tagsToPush ?? []} - isCherryPickInProgress={this.props.isCherryPickInProgress} onRenderCommitDragElement={this.onRenderCommitDragElement} onRemoveCommitDragElement={this.onRemoveCommitDragElement} disableSquashing={formState.kind === HistoryTabMode.Compare} + isMultiCommitOperationInProgress={ + this.props.isMultiCommitOperationInProgress + } /> ) } diff --git a/app/src/ui/repository.tsx b/app/src/ui/repository.tsx index 85e6e97484..10339c12ac 100644 --- a/app/src/ui/repository.tsx +++ b/app/src/ui/repository.tsx @@ -31,7 +31,6 @@ import { openFile } from './lib/open-file' import { AheadBehindStore } from '../lib/stores/ahead-behind-store' import { dragAndDropManager } from '../lib/drag-and-drop-manager' import { DragType } from '../models/drag-drop' -import { MultiCommitOperationKind } from '../models/multi-commit-operation' import { clamp } from '../lib/clamp' interface IRepositoryViewProps { @@ -250,10 +249,6 @@ export class RepositoryView extends React.Component< } = state const { tip } = branchesState const currentBranch = tip.kind === TipState.Valid ? tip.branch : null - const isCherryPickInProgress = - mcos !== null && - mcos.operationDetail.kind === MultiCommitOperationKind.CherryPick - const scrollTop = this.forceCompareListScrollTop || this.previousSection === RepositorySectionTab.Changes @@ -283,7 +278,7 @@ export class RepositoryView extends React.Component< compareListScrollTop={scrollTop} tagsToPush={tagsToPush} aheadBehindStore={aheadBehindStore} - isCherryPickInProgress={isCherryPickInProgress} + isMultiCommitOperationInProgress={mcos !== null} /> ) } From 2c6cae8c1cace54eb1df91c71347718cdc8f3c34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:08:50 +0000 Subject: [PATCH 104/262] Bump peter-evans/create-pull-request from 4.1.3 to 4.1.4 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.1.3 to 4.1.4. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v4.1.3...v4.1.4) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index c6daccbcec..c9a249428b 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -37,7 +37,7 @@ jobs: private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} - name: Create Release Pull Request - uses: peter-evans/create-pull-request@v4.1.3 + uses: peter-evans/create-pull-request@v4.1.4 if: | startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test') with: From ab9f11909e89de8f6660bad270a172063d775ac9 Mon Sep 17 00:00:00 2001 From: Angus Date: Mon, 17 Oct 2022 17:14:27 +0100 Subject: [PATCH 105/262] Refactoring extract the code to set this.mouseRect to rememberMousePosition --- app/src/ui/lib/tooltip.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/ui/lib/tooltip.tsx b/app/src/ui/lib/tooltip.tsx index 5d47f02ae4..1f65055187 100644 --- a/app/src/ui/lib/tooltip.tsx +++ b/app/src/ui/lib/tooltip.tsx @@ -299,8 +299,12 @@ export class Tooltip extends React.Component< } } - private onTargetMouseEnter = (event: MouseEvent) => { + private rememberMousePosition = (event: MouseEvent) => { this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20) + } + + private onTargetMouseEnter = (event: MouseEvent) => { + this.rememberMousePosition(event) this.mouseOverTarget = true this.cancelHideTooltip() @@ -310,7 +314,7 @@ export class Tooltip extends React.Component< } private onTargetMouseMove = (event: MouseEvent) => { - this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20) + this.rememberMousePosition(event) } private onTargetMouseDown = (event: MouseEvent) => { From afc2fac6255217adad816972412c2198d3b32718 Mon Sep 17 00:00:00 2001 From: Angus Date: Mon, 17 Oct 2022 18:29:44 +0100 Subject: [PATCH 106/262] Fix menu items are disabled if close the rebase conflict dialog onDismissed didn't reset IAppState. currentPopup so the menu is disabled. Need to call closePopup in onInvokeConflictsDialogDismissed "Abort Rebase" works properly since it calls onFlowEnded() --- .../ui/multi-commit-operation/base-multi-commit-operation.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/ui/multi-commit-operation/base-multi-commit-operation.tsx b/app/src/ui/multi-commit-operation/base-multi-commit-operation.tsx index aed59470f6..e3266ea7fe 100644 --- a/app/src/ui/multi-commit-operation/base-multi-commit-operation.tsx +++ b/app/src/ui/multi-commit-operation/base-multi-commit-operation.tsx @@ -93,6 +93,8 @@ export abstract class BaseMultiCommitOperation extends React.Component{targetBranch.name} : null} ) + + this.props.dispatcher.closePopup(PopupType.MultiCommitOperation) return dispatcher.onConflictsFoundBanner( repository, operationDescription, From 1315f8cc67b140b19c6c7bb75aa309d1ace9b516 Mon Sep 17 00:00:00 2001 From: Angus Date: Mon, 17 Oct 2022 22:35:24 +0100 Subject: [PATCH 107/262] Use text color for repository change indicator when selected and with focus --- app/styles/ui/_repository-list.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/styles/ui/_repository-list.scss b/app/styles/ui/_repository-list.scss index 044b1b8aa3..6adfa2d5b6 100644 --- a/app/styles/ui/_repository-list.scss +++ b/app/styles/ui/_repository-list.scss @@ -193,6 +193,12 @@ background: var(--list-item-selected-active-badge-background-color); color: var(--list-item-selected-active-badge-color); } + + .change-indicator-wrapper { + .octicon { + color: var(--text-color); + } + } } } From eddd8b9f5a579c3f969fb3b485cd9b1139ad1326 Mon Sep 17 00:00:00 2001 From: Angus Date: Tue, 18 Oct 2022 07:32:49 +0100 Subject: [PATCH 108/262] Update function name to updateMouseRect --- app/src/ui/lib/tooltip.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/ui/lib/tooltip.tsx b/app/src/ui/lib/tooltip.tsx index 1f65055187..a724608423 100644 --- a/app/src/ui/lib/tooltip.tsx +++ b/app/src/ui/lib/tooltip.tsx @@ -299,12 +299,12 @@ export class Tooltip extends React.Component< } } - private rememberMousePosition = (event: MouseEvent) => { + private updateMouseRect = (event: MouseEvent) => { this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20) } private onTargetMouseEnter = (event: MouseEvent) => { - this.rememberMousePosition(event) + this.updateMouseRect(event) this.mouseOverTarget = true this.cancelHideTooltip() @@ -314,7 +314,7 @@ export class Tooltip extends React.Component< } private onTargetMouseMove = (event: MouseEvent) => { - this.rememberMousePosition(event) + this.updateMouseRect(event) } private onTargetMouseDown = (event: MouseEvent) => { From 2a2ad0fcf4d0cdd116dd6ed3506ae787bf3624b6 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Tue, 18 Oct 2022 19:48:43 +0200 Subject: [PATCH 109/262] Update dugite to 2.1.0 This will get us Git v2.35.5 --- app/package.json | 2 +- app/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/package.json b/app/package.json index 9273708c62..adc12afd2d 100644 --- a/app/package.json +++ b/app/package.json @@ -30,7 +30,7 @@ "desktop-trampoline": "desktop/desktop-trampoline#v0.9.8", "dexie": "^3.2.2", "dompurify": "^2.3.3", - "dugite": "^2.0.0", + "dugite": "^2.1.0", "electron-window-state": "^5.0.3", "event-kit": "^2.0.0", "focus-trap-react": "^8.1.0", diff --git a/app/yarn.lock b/app/yarn.lock index fe461f8a78..4458395bd4 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -387,10 +387,10 @@ dompurify@^2.3.3: resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c" integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== -dugite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dugite/-/dugite-2.0.0.tgz#de0ef29b58caa863e25fecd318e99a0e6b05420b" - integrity sha512-0+1imU6NGzcvf42DDBMywZDyPBTNxKaG4IupDKfQbWU8S5fI+J3UVAQOrF0MNzGQYqv9G80Oz/LcS5043z05WQ== +dugite@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dugite/-/dugite-2.1.0.tgz#6f50c2244e57aaac2f36440aa7289815c73a688c" + integrity sha512-4l4jJz5zC6Q+/8doQNQZ9Ss3rmnO/JCHfOmQO+zGv+TIOUXimzfS02RvUOuFpEhZuaFTeFBSuK6ll/02TX3SxA== dependencies: progress "^2.0.3" tar "^6.1.11" From fbedef2bf17831ba0e1719970c32554bf5c0d6a1 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Tue, 18 Oct 2022 19:55:23 +0200 Subject: [PATCH 110/262] Changelog and package version bump --- app/package.json | 2 +- changelog.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index adc12afd2d..ec02226408 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "productName": "GitHub Desktop", "bundleID": "com.github.GitHubClient", "companyName": "GitHub, Inc.", - "version": "3.1.2-beta1", + "version": "3.1.3-beta1", "main": "./main.js", "repository": { "type": "git", diff --git a/changelog.json b/changelog.json index 4a57665760..806710fff4 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,6 @@ { "releases": { + "3.1.3-beta1": ["[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", From 11870edb893020628fc3b59a95ef96970cc68d73 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Tue, 18 Oct 2022 20:35:17 +0200 Subject: [PATCH 111/262] Revert file transport setting --- app/test/unit/git/rev-parse-test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/test/unit/git/rev-parse-test.ts b/app/test/unit/git/rev-parse-test.ts index a666dc7c7b..5e2c1b6428 100644 --- a/app/test/unit/git/rev-parse-test.ts +++ b/app/test/unit/git/rev-parse-test.ts @@ -57,7 +57,16 @@ describe('git/rev-parse', () => { secondRepoPath, '' ) - await git(['submodule', 'add', '../repo2'], firstRepoPath, '') + + await git( + [ + // Git 2.38 (backported into 2.35.5) changed the default here to 'user' + ...['-c', 'protocol.file.allow=always'], + ...['submodule', 'add', '../repo2'], + ], + firstRepoPath, + '' + ) expect(await getRepositoryType(firstRepoPath)).toMatchObject({ kind: 'regular', From eeec2730468db2adb14256bca01002c7532dc963 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 19 Oct 2022 12:17:31 +0200 Subject: [PATCH 112/262] Check if notification matches current repository --- app/src/lib/stores/notifications-store.ts | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index e88b6922c4..30f611b0a7 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -2,7 +2,9 @@ import { Repository, isRepositoryWithGitHubRepository, RepositoryWithGitHubRepository, + isRepositoryWithForkedGitHubRepository, } from '../../models/repository' +import { ForkContributionTarget } from '../../models/workflow-preferences' import { PullRequest } from '../../models/pull-request' import { API, APICheckConclusion } from '../api' import { @@ -121,6 +123,10 @@ export class NotificationsStore { return } + if (!this.isValidRepositoryForEvent(repository, event)) { + return + } + const pullRequests = await this.pullRequestCoordinator.getAllPullRequests( repository ) @@ -192,6 +198,10 @@ export class NotificationsStore { return } + if (!this.isValidRepositoryForEvent(repository, event)) { + return + } + const pullRequests = await this.pullRequestCoordinator.getAllPullRequests( repository ) @@ -283,6 +293,31 @@ export class NotificationsStore { this.statsStore.recordChecksFailedNotificationShown() } + 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) && + repository.workflowPreferences.forkContributionTarget === + 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 + ) + } + /** * Makes the store to keep track of the currently selected repository. Only * notifications for the currently selected repository will be shown. From 8f0c5895084a681d19c3b834800d5c94ae3b332b Mon Sep 17 00:00:00 2001 From: Daniel Ciaglia Date: Thu, 20 Oct 2022 12:14:13 +0200 Subject: [PATCH 113/262] satisfy prettier --- app/src/lib/editors/darwin.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/lib/editors/darwin.ts b/app/src/lib/editors/darwin.ts index 4376efb1a2..b9690c82c5 100644 --- a/app/src/lib/editors/darwin.ts +++ b/app/src/lib/editors/darwin.ts @@ -45,10 +45,7 @@ const editors: IDarwinExternalEditor[] = [ }, { name: 'VSCodium', - bundleIdentifiers: [ - 'com.visualstudio.code.oss', - 'com.vscodium', - ], + bundleIdentifiers: ['com.visualstudio.code.oss', 'com.vscodium'], }, { name: 'Sublime Text', From ba0cbcde3efc5df08675378402cf767f3361b1d8 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 18 Oct 2022 13:08:15 -0400 Subject: [PATCH 114/262] Extensify popups to add id's --- app/src/models/popup.ts | 743 +++++++++++++++++++++++++--------------- 1 file changed, 461 insertions(+), 282 deletions(-) diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 57cc87c866..cae0aba9ec 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -89,286 +89,465 @@ export enum PopupType { StartPullRequest, } +interface IBasePopup { + /** + * Unique id of the popup that it receives upon adding to the stack. + */ + readonly id?: string +} + +interface IRenameBranchPopup extends IBasePopup { + type: PopupType.RenameBranch + repository: Repository + branch: Branch +} + +interface IDeleteBranchPopup extends IBasePopup { + type: PopupType.DeleteBranch + repository: Repository + branch: Branch + existsOnRemote: boolean +} + +interface IDeleteRemoteBranchPopup extends IBasePopup { + type: PopupType.DeleteRemoteBranch + repository: Repository + branch: Branch +} + +interface IConfirmDiscardChangesPopup extends IBasePopup { + type: PopupType.ConfirmDiscardChanges + repository: Repository + files: ReadonlyArray + showDiscardChangesSetting?: boolean + discardingAllChanges?: boolean +} + +interface IConfirmDiscardSelectionPopup extends IBasePopup { + type: PopupType.ConfirmDiscardSelection + repository: Repository + file: WorkingDirectoryFileChange + diff: ITextDiff + selection: DiffSelection +} + +interface IPreferencesPopup extends IBasePopup { + type: PopupType.Preferences + initialSelectedTab?: PreferencesTab +} + +interface IRepositorySettingsPopup extends IBasePopup { + type: PopupType.RepositorySettings + repository: Repository + initialSelectedTab?: RepositorySettingsTab +} + +interface IAddRepositoryPopup extends IBasePopup { + type: PopupType.AddRepository + path?: string +} + +interface ICreateRepositoryPopup extends IBasePopup { + type: PopupType.CreateRepository + path?: string +} + +interface ICloneRepositoryPopup extends IBasePopup { + type: PopupType.CloneRepository + initialURL: string | null +} + +interface ICreateBranchPopup extends IBasePopup { + type: PopupType.CreateBranch + repository: Repository + initialName?: string + targetCommit?: CommitOneLine +} + +interface ISignInPopup extends IBasePopup { + type: PopupType.SignIn +} + +interface IAboutPopup extends IBasePopup { + type: PopupType.About +} + +interface IInstallGitPopup extends IBasePopup { + type: PopupType.InstallGit + path: string +} + +interface IPublishRepositoryPopup extends IBasePopup { + type: PopupType.PublishRepository + repository: Repository +} + +interface IAcknowledgementsPopup extends IBasePopup { + type: PopupType.Acknowledgements +} + +interface IUntrustedCertificatePopup extends IBasePopup { + type: PopupType.UntrustedCertificate + certificate: Electron.Certificate + url: string +} + +interface IRemoveRepositoryPopup extends IBasePopup { + type: PopupType.RemoveRepository + repository: Repository +} + +interface ITermsAndConditionsPopup extends IBasePopup { + type: PopupType.TermsAndConditions +} + +interface IPushBranchCommitsPopup extends IBasePopup { + type: PopupType.PushBranchCommits + repository: Repository + branch: Branch + unPushedCommits?: number +} + +interface ICLIInstalledPopup extends IBasePopup { + type: PopupType.CLIInstalled +} + +interface IGenericGitAuthenticationPopup extends IBasePopup { + type: PopupType.GenericGitAuthentication + hostname: string + retryAction: RetryAction +} + +interface IExternalEditorFailedPopup extends IBasePopup { + type: PopupType.ExternalEditorFailed + message: string + suggestDefaultEditor?: boolean + openPreferences?: boolean +} + +interface IOpenShellFailedPopup extends IBasePopup { + type: PopupType.OpenShellFailed + message: string +} + +interface IInitializeLFSPopup extends IBasePopup { + type: PopupType.InitializeLFS + repositories: ReadonlyArray +} + +interface ILFSAttributeMismatchPopup extends IBasePopup { + type: PopupType.LFSAttributeMismatch +} + +interface IUpstreamAlreadyExistsPopup extends IBasePopup { + type: PopupType.UpstreamAlreadyExists + repository: Repository + existingRemote: IRemote +} + +interface IReleaseNotesPopup extends IBasePopup { + type: PopupType.ReleaseNotes + newReleases: ReadonlyArray +} + +interface IDeletePullRequestPopup extends IBasePopup { + type: PopupType.DeletePullRequest + repository: Repository + branch: Branch + pullRequest: PullRequest +} + +interface IOversizedFilesPopup extends IBasePopup { + type: PopupType.OversizedFiles + oversizedFiles: ReadonlyArray + context: ICommitContext + repository: Repository +} + +interface ICommitConflictsWarningPopup extends IBasePopup { + type: PopupType.CommitConflictsWarning + /** files that were selected for committing that are also conflicted */ + files: ReadonlyArray + /** repository user is committing in */ + repository: Repository + /** information for completing the commit */ + context: ICommitContext +} + +interface IPushNeedsPullPopup extends IBasePopup { + type: PopupType.PushNeedsPull + repository: Repository +} + +interface IConfirmForcePushPopup extends IBasePopup { + type: PopupType.ConfirmForcePush + repository: Repository + upstreamBranch: string +} + +interface IStashAndSwitchBranchPopup extends IBasePopup { + type: PopupType.StashAndSwitchBranch + repository: Repository + branchToCheckout: Branch +} + +interface IConfirmOverwriteStashPopup extends IBasePopup { + type: PopupType.ConfirmOverwriteStash + repository: Repository + branchToCheckout: Branch | null +} + +interface IConfirmDiscardStashPopup extends IBasePopup { + type: PopupType.ConfirmDiscardStash + repository: Repository + stash: IStashEntry +} + +interface ICreateTutorialRepositoryPopup extends IBasePopup { + type: PopupType.CreateTutorialRepository + account: Account + progress?: Progress +} + +interface IConfirmExitTutorialPopup extends IBasePopup { + type: PopupType.ConfirmExitTutorial +} + +interface IPushRejectedDueToMissingWorkflowScopePopup extends IBasePopup { + type: PopupType.PushRejectedDueToMissingWorkflowScope + rejectedPath: string + repository: RepositoryWithGitHubRepository +} + +interface ISAMLReauthRequiredPopup extends IBasePopup { + type: PopupType.SAMLReauthRequired + organizationName: string + endpoint: string + retryAction?: RetryAction +} + +interface ICreateForkPopup extends IBasePopup { + type: PopupType.CreateFork + repository: RepositoryWithGitHubRepository + account: Account +} + +interface ICreateTagPopup extends IBasePopup { + type: PopupType.CreateTag + repository: Repository + targetCommitSha: string + initialName?: string + localTags: Map | null +} + +interface IDeleteTagPopup extends IBasePopup { + type: PopupType.DeleteTag + repository: Repository + tagName: string +} + +interface IChooseForkSettingsPopup extends IBasePopup { + type: PopupType.ChooseForkSettings + repository: RepositoryWithForkedGitHubRepository +} + +interface ILocalChangesOverwrittenPopup extends IBasePopup { + type: PopupType.LocalChangesOverwritten + repository: Repository + retryAction: RetryAction + files: ReadonlyArray +} + +interface IMoveToApplicationsFolderPopup extends IBasePopup { + type: PopupType.MoveToApplicationsFolder +} + +interface IChangeRepositoryAliasPopup extends IBasePopup { + type: PopupType.ChangeRepositoryAlias + repository: Repository +} + +interface IThankYouPopup extends IBasePopup { + type: PopupType.ThankYou + userContributions: ReadonlyArray + friendlyName: string + latestVersion: string | null +} + +interface ICommitMessagePopup extends IBasePopup { + type: PopupType.CommitMessage + coAuthors: ReadonlyArray + showCoAuthoredBy: boolean + commitMessage: ICommitMessage | null + dialogTitle: string + dialogButtonText: string + prepopulateCommitSummary: boolean + repository: Repository + onSubmitCommitMessage: (context: ICommitContext) => Promise +} + +interface IMultiCommitOperationPopup extends IBasePopup { + type: PopupType.MultiCommitOperation + repository: Repository +} + +interface IWarnLocalChangesBeforeUndoPopup extends IBasePopup { + type: PopupType.WarnLocalChangesBeforeUndo + repository: Repository + commit: Commit + isWorkingDirectoryClean: boolean +} + +interface IWarningBeforeResetPopup extends IBasePopup { + type: PopupType.WarningBeforeReset + repository: Repository + commit: Commit +} + +interface IInvalidatedTokenPopup extends IBasePopup { + type: PopupType.InvalidatedToken + account: Account +} + +interface IAddSSHHostPopup extends IBasePopup { + type: PopupType.AddSSHHost + host: string + ip: string + keyType: string + fingerprint: string + onSubmit: (addHost: boolean) => void +} + +interface ISSHKeyPassphrasePopup extends IBasePopup { + type: PopupType.SSHKeyPassphrase + keyPath: string + onSubmit: (passphrase: string | undefined, storePassphrase: boolean) => void +} + +interface ISSHUserPasswordPopup extends IBasePopup { + type: PopupType.SSHUserPassword + username: string + onSubmit: (password: string | undefined, storePassword: boolean) => void +} + +interface IPullRequestChecksFailedPopup extends IBasePopup { + type: PopupType.PullRequestChecksFailed + repository: RepositoryWithGitHubRepository + pullRequest: PullRequest + shouldChangeRepository: boolean + commitMessage: string + commitSha: string + checks: ReadonlyArray +} + +interface ICICheckRunRerunPopup extends IBasePopup { + type: PopupType.CICheckRunRerun + checkRuns: ReadonlyArray + repository: GitHubRepository + prRef: string + failedOnly: boolean +} + +interface IWarnForcePushPopup extends IBasePopup { + type: PopupType.WarnForcePush + operation: string + onBegin: () => void +} + +interface IDiscardChangesRetryPopup extends IBasePopup { + type: PopupType.DiscardChangesRetry + retryAction: RetryAction +} + +interface IPullRequestReviewPopup extends IBasePopup { + type: PopupType.PullRequestReview + repository: RepositoryWithGitHubRepository + pullRequest: PullRequest + review: ValidNotificationPullRequestReview + numberOfComments: number + shouldCheckoutBranch: boolean + shouldChangeRepository: boolean +} + +interface IUnreachableCommitsPopup extends IBasePopup { + type: PopupType.UnreachableCommits + selectedTab: UnreachableCommitsTab +} + +interface IStartPullRequestPopup extends IBasePopup { + type: PopupType.StartPullRequest + allBranches: ReadonlyArray + currentBranch: Branch + defaultBranch: Branch | null + externalEditorLabel?: string + imageDiffType: ImageDiffType + recentBranches: ReadonlyArray + repository: Repository + nonLocalCommitSHA: string | null + showSideBySideDiff: boolean +} + export type Popup = - | { type: PopupType.RenameBranch; repository: Repository; branch: Branch } - | { - type: PopupType.DeleteBranch - repository: Repository - branch: Branch - existsOnRemote: boolean - } - | { - type: PopupType.DeleteRemoteBranch - repository: Repository - branch: Branch - } - | { - type: PopupType.ConfirmDiscardChanges - repository: Repository - files: ReadonlyArray - showDiscardChangesSetting?: boolean - discardingAllChanges?: boolean - } - | { - type: PopupType.ConfirmDiscardSelection - repository: Repository - file: WorkingDirectoryFileChange - diff: ITextDiff - selection: DiffSelection - } - | { type: PopupType.Preferences; initialSelectedTab?: PreferencesTab } - | { - type: PopupType.RepositorySettings - repository: Repository - initialSelectedTab?: RepositorySettingsTab - } - | { type: PopupType.AddRepository; path?: string } - | { type: PopupType.CreateRepository; path?: string } - | { - type: PopupType.CloneRepository - initialURL: string | null - } - | { - type: PopupType.CreateBranch - repository: Repository - initialName?: string - targetCommit?: CommitOneLine - } - | { type: PopupType.SignIn } - | { type: PopupType.About } - | { type: PopupType.InstallGit; path: string } - | { type: PopupType.PublishRepository; repository: Repository } - | { type: PopupType.Acknowledgements } - | { - type: PopupType.UntrustedCertificate - certificate: Electron.Certificate - url: string - } - | { type: PopupType.RemoveRepository; repository: Repository } - | { type: PopupType.TermsAndConditions } - | { - type: PopupType.PushBranchCommits - repository: Repository - branch: Branch - unPushedCommits?: number - } - | { type: PopupType.CLIInstalled } - | { - type: PopupType.GenericGitAuthentication - hostname: string - retryAction: RetryAction - } - | { - type: PopupType.ExternalEditorFailed - message: string - suggestDefaultEditor?: boolean - openPreferences?: boolean - } - | { type: PopupType.OpenShellFailed; message: string } - | { type: PopupType.InitializeLFS; repositories: ReadonlyArray } - | { type: PopupType.LFSAttributeMismatch } - | { - type: PopupType.UpstreamAlreadyExists - repository: Repository - existingRemote: IRemote - } - | { - type: PopupType.ReleaseNotes - newReleases: ReadonlyArray - } - | { - type: PopupType.DeletePullRequest - repository: Repository - branch: Branch - pullRequest: PullRequest - } - | { - type: PopupType.OversizedFiles - oversizedFiles: ReadonlyArray - context: ICommitContext - repository: Repository - } - | { - type: PopupType.CommitConflictsWarning - /** files that were selected for committing that are also conflicted */ - files: ReadonlyArray - /** repository user is committing in */ - repository: Repository - /** information for completing the commit */ - context: ICommitContext - } - | { - type: PopupType.PushNeedsPull - repository: Repository - } - | { - type: PopupType.ConfirmForcePush - repository: Repository - upstreamBranch: string - } - | { - type: PopupType.StashAndSwitchBranch - repository: Repository - branchToCheckout: Branch - } - | { - type: PopupType.ConfirmOverwriteStash - repository: Repository - branchToCheckout: Branch | null - } - | { - type: PopupType.ConfirmDiscardStash - repository: Repository - stash: IStashEntry - } - | { - type: PopupType.CreateTutorialRepository - account: Account - progress?: Progress - } - | { - type: PopupType.ConfirmExitTutorial - } - | { - type: PopupType.PushRejectedDueToMissingWorkflowScope - rejectedPath: string - repository: RepositoryWithGitHubRepository - } - | { - type: PopupType.SAMLReauthRequired - organizationName: string - endpoint: string - retryAction?: RetryAction - } - | { - type: PopupType.CreateFork - repository: RepositoryWithGitHubRepository - account: Account - } - | { - type: PopupType.CreateTag - repository: Repository - targetCommitSha: string - initialName?: string - localTags: Map | null - } - | { - type: PopupType.DeleteTag - repository: Repository - tagName: string - } - | { - type: PopupType.ChooseForkSettings - repository: RepositoryWithForkedGitHubRepository - } - | { - type: PopupType.LocalChangesOverwritten - repository: Repository - retryAction: RetryAction - files: ReadonlyArray - } - | { type: PopupType.MoveToApplicationsFolder } - | { type: PopupType.ChangeRepositoryAlias; repository: Repository } - | { - type: PopupType.ThankYou - userContributions: ReadonlyArray - friendlyName: string - latestVersion: string | null - } - | { - type: PopupType.CommitMessage - coAuthors: ReadonlyArray - showCoAuthoredBy: boolean - commitMessage: ICommitMessage | null - dialogTitle: string - dialogButtonText: string - prepopulateCommitSummary: boolean - repository: Repository - onSubmitCommitMessage: (context: ICommitContext) => Promise - } - | { - type: PopupType.MultiCommitOperation - repository: Repository - } - | { - type: PopupType.WarnLocalChangesBeforeUndo - repository: Repository - commit: Commit - isWorkingDirectoryClean: boolean - } - | { - type: PopupType.WarningBeforeReset - repository: Repository - commit: Commit - } - | { - type: PopupType.InvalidatedToken - account: Account - } - | { - type: PopupType.AddSSHHost - host: string - ip: string - keyType: string - fingerprint: string - onSubmit: (addHost: boolean) => void - } - | { - type: PopupType.SSHKeyPassphrase - keyPath: string - onSubmit: ( - passphrase: string | undefined, - storePassphrase: boolean - ) => void - } - | { - type: PopupType.SSHUserPassword - username: string - onSubmit: (password: string | undefined, storePassword: boolean) => void - } - | { - type: PopupType.PullRequestChecksFailed - repository: RepositoryWithGitHubRepository - pullRequest: PullRequest - shouldChangeRepository: boolean - commitMessage: string - commitSha: string - checks: ReadonlyArray - } - | { - type: PopupType.CICheckRunRerun - checkRuns: ReadonlyArray - repository: GitHubRepository - prRef: string - failedOnly: boolean - } - | { type: PopupType.WarnForcePush; operation: string; onBegin: () => void } - | { - type: PopupType.DiscardChangesRetry - retryAction: RetryAction - } - | { - type: PopupType.PullRequestReview - repository: RepositoryWithGitHubRepository - pullRequest: PullRequest - review: ValidNotificationPullRequestReview - numberOfComments: number - shouldCheckoutBranch: boolean - shouldChangeRepository: boolean - } - | { - type: PopupType.UnreachableCommits - selectedTab: UnreachableCommitsTab - } - | { - type: PopupType.StartPullRequest - allBranches: ReadonlyArray - currentBranch: Branch - defaultBranch: Branch | null - externalEditorLabel?: string - imageDiffType: ImageDiffType - recentBranches: ReadonlyArray - repository: Repository - nonLocalCommitSHA: string | null - showSideBySideDiff: boolean - } + | IRenameBranchPopup + | IDeleteBranchPopup + | IDeleteRemoteBranchPopup + | IConfirmDiscardChangesPopup + | IConfirmDiscardSelectionPopup + | IPreferencesPopup + | IRepositorySettingsPopup + | IAddRepositoryPopup + | ICreateRepositoryPopup + | ICloneRepositoryPopup + | ICreateBranchPopup + | ISignInPopup + | IAboutPopup + | IInstallGitPopup + | IPublishRepositoryPopup + | IAcknowledgementsPopup + | IUntrustedCertificatePopup + | IRemoveRepositoryPopup + | ITermsAndConditionsPopup + | IPushBranchCommitsPopup + | ICLIInstalledPopup + | IGenericGitAuthenticationPopup + | IExternalEditorFailedPopup + | IOpenShellFailedPopup + | IInitializeLFSPopup + | ILFSAttributeMismatchPopup + | IUpstreamAlreadyExistsPopup + | IReleaseNotesPopup + | IDeletePullRequestPopup + | IOversizedFilesPopup + | ICommitConflictsWarningPopup + | IPushNeedsPullPopup + | IConfirmForcePushPopup + | IStashAndSwitchBranchPopup + | IConfirmOverwriteStashPopup + | IConfirmDiscardStashPopup + | ICreateTutorialRepositoryPopup + | IConfirmExitTutorialPopup + | IPushRejectedDueToMissingWorkflowScopePopup + | ISAMLReauthRequiredPopup + | ICreateForkPopup + | ICreateTagPopup + | IDeleteTagPopup + | IChooseForkSettingsPopup + | ILocalChangesOverwrittenPopup + | IMoveToApplicationsFolderPopup + | IChangeRepositoryAliasPopup + | IThankYouPopup + | ICommitMessagePopup + | IMultiCommitOperationPopup + | IWarnLocalChangesBeforeUndoPopup + | IWarningBeforeResetPopup + | IInvalidatedTokenPopup + | IAddSSHHostPopup + | ISSHKeyPassphrasePopup + | ISSHUserPasswordPopup + | IPullRequestChecksFailedPopup + | ICICheckRunRerunPopup + | IWarnForcePushPopup + | IDiscardChangesRetryPopup + | IPullRequestReviewPopup + | IUnreachableCommitsPopup + | IStartPullRequestPopup From 814cc5b5af6e9ff7585a263f0e8ac2efa5e21eb0 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 18 Oct 2022 13:08:31 -0400 Subject: [PATCH 115/262] Create a popup manager --- app/src/lib/popup-manager.ts | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 app/src/lib/popup-manager.ts diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts new file mode 100644 index 0000000000..8202cc073f --- /dev/null +++ b/app/src/lib/popup-manager.ts @@ -0,0 +1,65 @@ +import { Popup, PopupType } from '../models/popup' +import { getObjectId } from '../ui/lib/object-id' + +/** + * The popup manager is to manage the stack of currently open popups. + */ +export class PopupManager { + private popupStack = new Array() + + public get currentPopup(): Popup | undefined { + return this.popupStack.at(-1) + } + + public get isAPopupOpen(): boolean { + return this.currentPopup !== undefined + } + + public getPopupsOfType(popupType: PopupType): ReadonlyArray { + return [...this.popupStack.filter(p => p.type !== popupType)] + } + + public isPopupsOfType(popupType: PopupType): boolean { + return this.popupStack.some(p => p.type === popupType) + } + + public addPopup(popupToAdd: Popup): Popup { + const popup = { id: getObjectId(popupToAdd), ...popupToAdd } + this.popupStack.push(popup) + return popup + } + + public updatePopup(popupToUpdate: Popup): Popup { + if (popupToUpdate.id === null) { + console.warn(`Attempted to update a popup without an id.`) + return popupToUpdate + } + + const index = this.popupStack.findIndex(p => p.id === popupToUpdate.id) + if (index < 0) { + console.warn(`Attempted to update a popup not in the stack.`) + return popupToUpdate + } + + this.popupStack = [ + ...this.popupStack.slice(0, index), + popupToUpdate, + ...this.popupStack.slice(index + 1), + ] + return popupToUpdate + } + + public removePopup(popup: Popup) { + if (popup.id === null) { + console.warn(`Attempted to remove a popup without an id.`) + return + } + this.popupStack = this.popupStack.filter(p => p.id !== popup.id) + } + + public removePopupByType(popupType: PopupType) { + this.popupStack = this.popupStack.filter(p => p.type !== popupType) + } +} + +export const popupManager = new PopupManager() From 1109720cbf33442765d89875e13874aa70be77ef Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 19 Oct 2022 10:05:52 -0400 Subject: [PATCH 116/262] Use popup manager --- app/src/lib/app-state.ts | 2 - app/src/lib/menu-update.ts | 3 +- app/src/lib/multi-commit-operation.ts | 7 ++-- app/src/lib/stores/app-store.ts | 60 ++++++++++++--------------- app/src/ui/app.tsx | 5 ++- app/src/ui/index.tsx | 5 ++- 6 files changed, 38 insertions(+), 44 deletions(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index 08ead91bc3..952da275d1 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -22,7 +22,6 @@ import { ICloneProgress, IMultiCommitOperationProgress, } from '../models/progress' -import { Popup } from '../models/popup' import { SignInState } from './stores/sign-in-store' @@ -115,7 +114,6 @@ export interface IAppState { readonly showWelcomeFlow: boolean readonly focusCommitMessage: boolean - readonly currentPopup: Popup | null readonly currentFoldout: Foldout | null readonly currentBanner: Banner | null diff --git a/app/src/lib/menu-update.ts b/app/src/lib/menu-update.ts index 9f0c644679..c4825b0de1 100644 --- a/app/src/lib/menu-update.ts +++ b/app/src/lib/menu-update.ts @@ -11,6 +11,7 @@ import { updateMenuState as ipcUpdateMenuState } from '../ui/main-process-proxy' import { AppMenu, MenuItem } from '../models/app-menu' import { hasConflictedFiles } from './status' import { findContributionTargetDefaultBranch } from './branch' +import { popupManager } from './popup-manager' export interface IMenuItemState { readonly enabled?: boolean @@ -363,7 +364,7 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder { } function getMenuState(state: IAppState): Map { - if (state.currentPopup) { + if (popupManager.isAPopupOpen) { return getAllMenusDisabledBuilder().state } diff --git a/app/src/lib/multi-commit-operation.ts b/app/src/lib/multi-commit-operation.ts index 897c2909e2..8b88338126 100644 --- a/app/src/lib/multi-commit-operation.ts +++ b/app/src/lib/multi-commit-operation.ts @@ -4,9 +4,10 @@ import { conflictSteps, MultiCommitOperationStepKind, } from '../models/multi-commit-operation' -import { Popup, PopupType } from '../models/popup' +import { PopupType } from '../models/popup' import { TipState } from '../models/tip' import { IMultiCommitOperationState, IRepositoryState } from './app-state' +import { popupManager } from './popup-manager' /** * Setup the multi commit operation state when the user needs to select a branch as the @@ -39,12 +40,10 @@ export function getMultiCommitOperationChooseBranchStep( } export function isConflictsFlow( - currentPopup: Popup | null, multiCommitOperationState: IMultiCommitOperationState | null ): boolean { return ( - currentPopup !== null && - currentPopup.type === PopupType.MultiCommitOperation && + popupManager.isPopupsOfType(PopupType.MultiCommitOperation) && multiCommitOperationState !== null && conflictSteps.includes(multiCommitOperationState.step.kind) ) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index afed3ee5c9..4aae35489d 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -304,6 +304,7 @@ import { offsetFromNow } from '../offset-from' import { findContributionTargetDefaultBranch } from '../branch' import { ValidNotificationPullRequestReview } from '../valid-notification-pull-request-review' import { determineMergeability } from '../git/merge-tree' +import { popupManager } from '../popup-manager' const LastSelectedRepositoryIDKey = 'last-selected-repository-id' @@ -396,7 +397,6 @@ export class AppStore extends TypedBaseStore { private showWelcomeFlow = false private focusCommitMessage = false - private currentPopup: Popup | null = null private currentFoldout: Foldout | null = null private currentBanner: Banner | null = null private errors: ReadonlyArray = new Array() @@ -639,7 +639,7 @@ export class AppStore extends TypedBaseStore { // If there is a currently open popup, don't do anything here. Since the // app can only show one popup at a time, we don't want to close the current // one in favor of the error we're about to show. - if (this.currentPopup !== null) { + if (popupManager.isAPopupOpen) { return } @@ -904,7 +904,6 @@ export class AppStore extends TypedBaseStore { appIsFocused: this.appIsFocused, selectedState: this.getSelectedState(), signInState: this.signInStore.getState(), - currentPopup: this.currentPopup, currentFoldout: this.currentFoldout, errors: this.errors, showWelcomeFlow: this.showWelcomeFlow, @@ -2506,10 +2505,7 @@ export class AppStore extends TypedBaseStore { this.currentBanner !== null && this.currentBanner.type === BannerType.ConflictsFound - if ( - displayingBanner || - isConflictsFlow(this.currentPopup, multiCommitOperationState) - ) { + if (displayingBanner || isConflictsFlow(multiCommitOperationState)) { return } @@ -2596,12 +2592,7 @@ export class AppStore extends TypedBaseStore { */ private clearConflictsFlowVisuals(state: IRepositoryState) { const { multiCommitOperationState } = state - if ( - userIsStartingMultiCommitOperation( - this.currentPopup, - multiCommitOperationState - ) - ) { + if (userIsStartingMultiCommitOperation(multiCommitOperationState)) { return } @@ -3480,32 +3471,35 @@ export class AppStore extends TypedBaseStore { /** This shouldn't be called directly. See `Dispatcher`. */ public async _showPopup(popup: Popup): Promise { - this._closePopup() - // Always close the app menu when showing a pop up. This is only // applicable on Windows where we draw a custom app menu. this._closeFoldout(FoldoutType.AppMenu) - this.currentPopup = popup + popupManager.addPopup(popup) this.emitUpdate() } /** This shouldn't be called directly. See `Dispatcher`. */ public _closePopup(popupType?: PopupType) { - const currentPopup = this.currentPopup - if (currentPopup == null) { + const currentPopup = popupManager.currentPopup + if (currentPopup === undefined) { return } - if (popupType !== undefined && currentPopup.type !== popupType) { - return + if (popupType === undefined) { + popupManager.removePopup(currentPopup) + } else { + if (currentPopup.type !== popupType) { + return + } + + if (currentPopup.type === PopupType.CloneRepository) { + this._completeOpenInDesktop(() => Promise.resolve(null)) + } + + popupManager.removePopupByType(popupType) } - if (currentPopup.type === PopupType.CloneRepository) { - this._completeOpenInDesktop(() => Promise.resolve(null)) - } - - this.currentPopup = null this.emitUpdate() } @@ -6497,13 +6491,14 @@ export class AppStore extends TypedBaseStore { path, (title, value, description) => { if ( - this.currentPopup !== null && - this.currentPopup.type === PopupType.CreateTutorialRepository + popupManager.currentPopup !== undefined && + popupManager.currentPopup.type === + PopupType.CreateTutorialRepository ) { - this.currentPopup = { - ...this.currentPopup, + popupManager.updatePopup({ + ...popupManager.currentPopup, progress: { kind: 'generic', title, value, description }, - } + }) this.emitUpdate() } } @@ -7488,14 +7483,13 @@ function getInitialAction( } function userIsStartingMultiCommitOperation( - currentPopup: Popup | null, state: IMultiCommitOperationState | null ) { - if (currentPopup === null || state === null) { + if (!popupManager.isAPopupOpen || state === null) { return false } - if (currentPopup.type !== PopupType.MultiCommitOperation) { + if (popupManager.currentPopup?.type !== PopupType.MultiCommitOperation) { return false } diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 8a97914cbf..e8fe72a6e2 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -159,6 +159,7 @@ import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog' import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-dialog' import { sendNonFatalException } from '../lib/helpers/non-fatal-exception' import { createCommitURL } from '../lib/commit-url' +import { popupManager } from '../lib/popup-manager' const MinuteInMilliseconds = 1000 * 60 const HourInMilliseconds = MinuteInMilliseconds * 60 @@ -215,7 +216,7 @@ export class App extends React.Component { * modal dialog such as the preferences, or an error dialog. */ private get isShowingModal() { - return this.state.currentPopup !== null || this.state.errors.length > 0 + return popupManager.isAPopupOpen || this.state.errors.length > 0 } /** @@ -1375,7 +1376,7 @@ export class App extends React.Component { return null } - const popup = this.state.currentPopup + const popup = popupManager.currentPopup if (!popup) { return null diff --git a/app/src/ui/index.tsx b/app/src/ui/index.tsx index 2d883d5606..8f037dcfc9 100644 --- a/app/src/ui/index.tsx +++ b/app/src/ui/index.tsx @@ -79,6 +79,7 @@ import { NotificationsStore } from '../lib/stores/notifications-store' import * as ipcRenderer from '../lib/ipc-renderer' import { migrateRendererGUID } from '../lib/get-renderer-guid' import { initializeRendererNotificationHandler } from '../lib/notifications/notification-handler' +import { popupManager } from '../lib/popup-manager' if (__DEV__) { installDevGlobals() @@ -144,8 +145,8 @@ const sendErrorWithContext = ( extra.currentBanner = currentState.currentBanner.type } - if (currentState.currentPopup !== null) { - extra.currentPopup = `${currentState.currentPopup.type}` + if (popupManager.currentPopup !== undefined) { + extra.currentPopup = `${popupManager.currentPopup.type}` } if (currentState.selectedState !== null) { From 59a3c86d554434f59c3da82d59a62889870e1c9f Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 19 Oct 2022 12:10:13 -0400 Subject: [PATCH 117/262] Use logger, fix popuptypes, use uuid --- app/src/lib/popup-manager.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 8202cc073f..c0719adb1c 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -1,5 +1,5 @@ +import uuid from 'uuid' import { Popup, PopupType } from '../models/popup' -import { getObjectId } from '../ui/lib/object-id' /** * The popup manager is to manage the stack of currently open popups. @@ -16,7 +16,7 @@ export class PopupManager { } public getPopupsOfType(popupType: PopupType): ReadonlyArray { - return [...this.popupStack.filter(p => p.type !== popupType)] + return [...this.popupStack.filter(p => p.type === popupType)] } public isPopupsOfType(popupType: PopupType): boolean { @@ -24,20 +24,26 @@ export class PopupManager { } public addPopup(popupToAdd: Popup): Popup { - const popup = { id: getObjectId(popupToAdd), ...popupToAdd } + const existingPopup = this.getPopupsOfType(popupToAdd.type) + if (existingPopup.length > 0) { + log.warn(`Attempted to add a popup of already existing type.`) + return popupToAdd + } + + const popup = { id: uuid(), ...popupToAdd } this.popupStack.push(popup) return popup } public updatePopup(popupToUpdate: Popup): Popup { if (popupToUpdate.id === null) { - console.warn(`Attempted to update a popup without an id.`) + log.warn(`Attempted to update a popup without an id.`) return popupToUpdate } const index = this.popupStack.findIndex(p => p.id === popupToUpdate.id) if (index < 0) { - console.warn(`Attempted to update a popup not in the stack.`) + log.warn(`Attempted to update a popup not in the stack.`) return popupToUpdate } @@ -51,7 +57,7 @@ export class PopupManager { public removePopup(popup: Popup) { if (popup.id === null) { - console.warn(`Attempted to remove a popup without an id.`) + log.warn(`Attempted to remove a popup without an id.`) return } this.popupStack = this.popupStack.filter(p => p.id !== popup.id) From 6f9b2638bd94e19a179eae242fb4f9949263e45d Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 19 Oct 2022 13:16:42 -0400 Subject: [PATCH 118/262] Documenting --- app/src/lib/popup-manager.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index c0719adb1c..d1bd0c509a 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -7,22 +7,39 @@ import { Popup, PopupType } from '../models/popup' export class PopupManager { private popupStack = new Array() + /** + * Returns the last popup added to the stack. + */ public get currentPopup(): Popup | undefined { return this.popupStack.at(-1) } + /** + * Returns whether there are any popups in the stack. + */ public get isAPopupOpen(): boolean { return this.currentPopup !== undefined } + /** + * Returns an array of all popups in the stack of the provided type. + **/ public getPopupsOfType(popupType: PopupType): ReadonlyArray { return [...this.popupStack.filter(p => p.type === popupType)] } + /** + * Returns whether there are any popups of a given type in the stack. + */ public isPopupsOfType(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 to the stack + **/ public addPopup(popupToAdd: Popup): Popup { const existingPopup = this.getPopupsOfType(popupToAdd.type) if (existingPopup.length > 0) { @@ -35,6 +52,10 @@ export class PopupManager { return popup } + /** + * Updates a popup in the stack and returns it. + * - It uses the popup id to find and update the popup. + */ public updatePopup(popupToUpdate: Popup): Popup { if (popupToUpdate.id === null) { log.warn(`Attempted to update a popup without an id.`) @@ -55,6 +76,9 @@ export class PopupManager { return popupToUpdate } + /** + * Removes a popup based on it's id. + */ public removePopup(popup: Popup) { if (popup.id === null) { log.warn(`Attempted to remove a popup without an id.`) @@ -63,6 +87,9 @@ export class PopupManager { 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) } From e8aa39a064728b0a10b33d4b94327a3328515b62 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Thu, 20 Oct 2022 14:37:05 -0400 Subject: [PATCH 119/262] Correct null/undefined check --- app/src/lib/popup-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index d1bd0c509a..7606ccb83b 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -57,7 +57,7 @@ export class PopupManager { * - It uses the popup id to find and update the popup. */ public updatePopup(popupToUpdate: Popup): Popup { - if (popupToUpdate.id === null) { + if (popupToUpdate.id === undefined) { log.warn(`Attempted to update a popup without an id.`) return popupToUpdate } From fe1f46fc2f2dc4f1a7dfd27a01fdfbbdd9c72fc4 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Thu, 20 Oct 2022 14:37:35 -0400 Subject: [PATCH 120/262] Add popup stack limit --- app/src/lib/popup-manager.ts | 24 ++++++- app/src/models/popup.ts | 126 +++++++++++++++++------------------ 2 files changed, 86 insertions(+), 64 deletions(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 7606ccb83b..7253adf412 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -1,5 +1,13 @@ import uuid from 'uuid' import { Popup, PopupType } from '../models/popup' +import { sendNonFatalException } from './helpers/non-fatal-exception' + +/** + * 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 popupStackLimit = 50 /** * The popup manager is to manage the stack of currently open popups. @@ -43,12 +51,26 @@ export class PopupManager { public addPopup(popupToAdd: Popup): Popup { const existingPopup = this.getPopupsOfType(popupToAdd.type) if (existingPopup.length > 0) { - log.warn(`Attempted to add a popup of already existing type.`) + log.warn( + `Attempted to add a popup of already existing type - ${popupToAdd.type}.` + ) return popupToAdd } const popup = { id: uuid(), ...popupToAdd } this.popupStack.push(popup) + + if (this.popupStack.length > popupStackLimit) { + // Remove the oldest + const oldest = this.popupStack[0] + sendNonFatalException( + 'TooManyPopups', + new Error( + `Max number of ${popupStackLimit} popups reached while adding popup of type ${popup.type}. Removing last popup from the stack -> type ${oldest.type} ` + ) + ) + this.popupStack = this.popupStack.slice(1) + } return popup } diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index cae0aba9ec..3bdab997a5 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -24,69 +24,69 @@ import { ValidNotificationPullRequestReview } from '../lib/valid-notification-pu import { UnreachableCommitsTab } from '../ui/history/unreachable-commits-dialog' export enum PopupType { - RenameBranch = 1, - DeleteBranch, - DeleteRemoteBranch, - ConfirmDiscardChanges, - Preferences, - RepositorySettings, - AddRepository, - CreateRepository, - CloneRepository, - CreateBranch, - SignIn, - About, - InstallGit, - PublishRepository, - Acknowledgements, - UntrustedCertificate, - RemoveRepository, - TermsAndConditions, - PushBranchCommits, - CLIInstalled, - GenericGitAuthentication, - ExternalEditorFailed, - OpenShellFailed, - InitializeLFS, - LFSAttributeMismatch, - UpstreamAlreadyExists, - ReleaseNotes, - DeletePullRequest, - OversizedFiles, - CommitConflictsWarning, - PushNeedsPull, - ConfirmForcePush, - StashAndSwitchBranch, - ConfirmOverwriteStash, - ConfirmDiscardStash, - CreateTutorialRepository, - ConfirmExitTutorial, - PushRejectedDueToMissingWorkflowScope, - SAMLReauthRequired, - CreateFork, - CreateTag, - DeleteTag, - LocalChangesOverwritten, - ChooseForkSettings, - ConfirmDiscardSelection, - MoveToApplicationsFolder, - ChangeRepositoryAlias, - ThankYou, - CommitMessage, - MultiCommitOperation, - WarnLocalChangesBeforeUndo, - WarningBeforeReset, - InvalidatedToken, - AddSSHHost, - SSHKeyPassphrase, - SSHUserPassword, - PullRequestChecksFailed, - CICheckRunRerun, - WarnForcePush, - DiscardChangesRetry, - PullRequestReview, - UnreachableCommits, - StartPullRequest, + RenameBranch = 'RenameBranch', + DeleteBranch = 'DeleteBranch', + DeleteRemoteBranch = 'DeleteRemoteBranch', + ConfirmDiscardChanges = 'ConfirmDiscardChanges', + Preferences = 'Preferences', + RepositorySettings = 'RepositorySettings', + AddRepository = 'AddRepository', + CreateRepository = 'CreateRepository', + CloneRepository = 'CloneRepository', + CreateBranch = 'CreateBranch', + SignIn = 'SignIn', + About = 'About', + InstallGit = 'InstallGit', + PublishRepository = 'PublishRepository', + Acknowledgements = 'Acknowledgements', + UntrustedCertificate = 'UntrustedCertificate', + RemoveRepository = 'RemoveRepository', + TermsAndConditions = 'TermsAndConditions', + PushBranchCommits = 'PushBranchCommits', + CLIInstalled = 'CLIInstalled', + GenericGitAuthentication = 'GenericGitAuthentication', + ExternalEditorFailed = 'ExternalEditorFailed', + OpenShellFailed = 'OpenShellFailed', + InitializeLFS = 'InitializeLFS', + LFSAttributeMismatch = 'LFSAttributeMismatch', + UpstreamAlreadyExists = 'UpstreamAlreadyExists', + ReleaseNotes = 'ReleaseNotes', + DeletePullRequest = 'DeletePullRequest', + OversizedFiles = 'OversizedFiles', + CommitConflictsWarning = 'CommitConflictsWarning', + PushNeedsPull = 'PushNeedsPull', + ConfirmForcePush = 'ConfirmForcePush', + StashAndSwitchBranch = 'StashAndSwitchBranch', + ConfirmOverwriteStash = 'ConfirmOverwriteStash', + ConfirmDiscardStash = 'ConfirmDiscardStash', + CreateTutorialRepository = 'CreateTutorialRepository', + ConfirmExitTutorial = 'ConfirmExitTutorial', + PushRejectedDueToMissingWorkflowScope = 'PushRejectedDueToMissingWorkflowScope', + SAMLReauthRequired = 'SAMLReauthRequired', + CreateFork = 'CreateFork', + CreateTag = 'CreateTag', + DeleteTag = 'DeleteTag', + LocalChangesOverwritten = 'LocalChangesOverwritten', + ChooseForkSettings = 'ChooseForkSettings', + ConfirmDiscardSelection = 'ConfirmDiscardSelection', + MoveToApplicationsFolder = 'MoveToApplicationsFolder', + ChangeRepositoryAlias = 'ChangeRepositoryAlias', + ThankYou = 'ThankYou', + CommitMessage = 'CommitMessage', + MultiCommitOperation = 'MultiCommitOperation', + WarnLocalChangesBeforeUndo = 'WarnLocalChangesBeforeUndo', + WarningBeforeReset = 'WarningBeforeReset', + InvalidatedToken = 'InvalidatedToken', + AddSSHHost = 'AddSSHHost', + SSHKeyPassphrase = 'SSHKeyPassphrase', + SSHUserPassword = 'SSHUserPassword', + PullRequestChecksFailed = 'PullRequestChecksFailed', + CICheckRunRerun = 'CICheckRunRerun', + WarnForcePush = 'WarnForcePush', + DiscardChangesRetry = 'DiscardChangesRetry', + PullRequestReview = 'PullRequestReview', + UnreachableCommits = 'UnreachableCommits', + StartPullRequest = 'StartPullRequest', } interface IBasePopup { From 1bee22cc75a031928b6168c3e205645974a17b65 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 20 Oct 2022 15:25:37 -0400 Subject: [PATCH 121/262] grammars.. --- app/src/lib/multi-commit-operation.ts | 2 +- app/src/lib/popup-manager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/lib/multi-commit-operation.ts b/app/src/lib/multi-commit-operation.ts index 8b88338126..8694cdf0ae 100644 --- a/app/src/lib/multi-commit-operation.ts +++ b/app/src/lib/multi-commit-operation.ts @@ -43,7 +43,7 @@ export function isConflictsFlow( multiCommitOperationState: IMultiCommitOperationState | null ): boolean { return ( - popupManager.isPopupsOfType(PopupType.MultiCommitOperation) && + popupManager.arePopupsOfType(PopupType.MultiCommitOperation) && multiCommitOperationState !== null && conflictSteps.includes(multiCommitOperationState.step.kind) ) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 7253adf412..f2f7ce3047 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -39,7 +39,7 @@ export class PopupManager { /** * Returns whether there are any popups of a given type in the stack. */ - public isPopupsOfType(popupType: PopupType): boolean { + public arePopupsOfType(popupType: PopupType): boolean { return this.popupStack.some(p => p.type === popupType) } From 189767e3945194b26360c45f9860e83a8a4be7f4 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 21 Oct 2022 13:23:51 +0200 Subject: [PATCH 122/262] Full size PR create dialog --- app/styles/ui/_dialog.scss | 21 ++++++++++++++----- .../ui/_pull-request-files-changed.scss | 6 +++++- app/styles/ui/dialogs/_open-pull-request.scss | 9 ++++++-- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/styles/ui/_dialog.scss b/app/styles/ui/_dialog.scss index f9e5550a87..c317f7bfd9 100644 --- a/app/styles/ui/_dialog.scss +++ b/app/styles/ui/_dialog.scss @@ -27,6 +27,9 @@ // allow easy layout using generalized components and elements such as // and

. dialog { + display: flex; + flex-direction: column; + overflow: unset; // These are the 24px versions of the alert and stop octicons // from oction v10.0.0 @@ -128,11 +131,19 @@ dialog { // The dialog embeds a fieldset as the first child of the form element // in order to be able to disable all form elements and buttons in one // swoop. This resets all styles for that fieldset. - & > form > fieldset { - border: 0; - margin: 0; - padding: 0; - min-width: 0; + & > form { + min-height: 0; + & > fieldset { + border: 0; + margin: 0; + padding: 0; + min-width: 0; + min-height: 0; + max-height: 100%; + + display: flex; + flex-direction: column; + } } .dialog-header { diff --git a/app/styles/ui/_pull-request-files-changed.scss b/app/styles/ui/_pull-request-files-changed.scss index 57f04d82a5..c432c65fe0 100644 --- a/app/styles/ui/_pull-request-files-changed.scss +++ b/app/styles/ui/_pull-request-files-changed.scss @@ -1,4 +1,8 @@ .pull-request-files-changed { + display: flex; + flex-direction: column; + min-height: 0; + border: var(--base-border); border-radius: var(--border-radius); @@ -13,8 +17,8 @@ } .files-diff-viewer { - height: 500px; display: flex; + min-height: 0; } .file-list { diff --git a/app/styles/ui/dialogs/_open-pull-request.scss b/app/styles/ui/dialogs/_open-pull-request.scss index 7534652ca0..443c23c3d9 100644 --- a/app/styles/ui/dialogs/_open-pull-request.scss +++ b/app/styles/ui/dialogs/_open-pull-request.scss @@ -1,6 +1,8 @@ .open-pull-request { - width: 850px; - max-width: none; + width: 100%; + height: 100%; + max-width: calc(100% - var(--spacing-double) * 4); + max-height: calc(100% - var(--spacing-double) * 4); header.dialog-header { padding-bottom: var(--spacing); @@ -21,6 +23,9 @@ .open-pull-request-content { padding: var(--spacing); + display: flex; + flex-direction: column; + min-height: 0; } .open-pull-request-no-changes { From d238d5e0bc313da6993617f98770b171134770fc Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 21 Oct 2022 14:03:06 +0200 Subject: [PATCH 123/262] Expand to fill available height --- app/styles/ui/_dialog.scss | 2 ++ app/styles/ui/_pull-request-files-changed.scss | 2 ++ app/styles/ui/dialogs/_open-pull-request.scss | 1 + 3 files changed, 5 insertions(+) diff --git a/app/styles/ui/_dialog.scss b/app/styles/ui/_dialog.scss index c317f7bfd9..a05fd82dd1 100644 --- a/app/styles/ui/_dialog.scss +++ b/app/styles/ui/_dialog.scss @@ -133,6 +133,7 @@ dialog { // swoop. This resets all styles for that fieldset. & > form { min-height: 0; + height: 100%; & > fieldset { border: 0; margin: 0; @@ -140,6 +141,7 @@ dialog { min-width: 0; min-height: 0; max-height: 100%; + height: 100%; display: flex; flex-direction: column; diff --git a/app/styles/ui/_pull-request-files-changed.scss b/app/styles/ui/_pull-request-files-changed.scss index c432c65fe0..55d72dadbd 100644 --- a/app/styles/ui/_pull-request-files-changed.scss +++ b/app/styles/ui/_pull-request-files-changed.scss @@ -2,6 +2,7 @@ display: flex; flex-direction: column; min-height: 0; + flex-grow: 1; border: var(--base-border); border-radius: var(--border-radius); @@ -19,6 +20,7 @@ .files-diff-viewer { display: flex; min-height: 0; + flex-grow: 1; } .file-list { diff --git a/app/styles/ui/dialogs/_open-pull-request.scss b/app/styles/ui/dialogs/_open-pull-request.scss index 443c23c3d9..bb69524da2 100644 --- a/app/styles/ui/dialogs/_open-pull-request.scss +++ b/app/styles/ui/dialogs/_open-pull-request.scss @@ -26,6 +26,7 @@ display: flex; flex-direction: column; min-height: 0; + flex-grow: 1; } .open-pull-request-no-changes { From 11d84577fa1898ccf98343e21244518cc08c08b4 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 21 Oct 2022 14:37:00 +0200 Subject: [PATCH 124/262] Refresh codemirror once the dialog has appeared fully --- app/src/ui/dialog/dialog.tsx | 7 +++++++ app/src/ui/diff/code-mirror-host.tsx | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index 9f9c27b1a1..3ac49972e0 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -214,6 +214,13 @@ export class Dialog extends React.Component { private onDismissGraceTimer = () => { this.setState({ isAppearing: false }) + + this.dialogElement?.dispatchEvent( + new CustomEvent('dialog-appeared', { + bubbles: true, + cancelable: false, + }) + ) } private isDismissable() { diff --git a/app/src/ui/diff/code-mirror-host.tsx b/app/src/ui/diff/code-mirror-host.tsx index d840a34274..ab018444cf 100644 --- a/app/src/ui/diff/code-mirror-host.tsx +++ b/app/src/ui/diff/code-mirror-host.tsx @@ -164,6 +164,12 @@ export class CodeMirrorHost extends React.Component { CodeMirrorHost.updateDoc(this.codeMirror, this.props.value) this.resizeObserver.observe(this.codeMirror.getWrapperElement()) + + document.addEventListener('dialog-appeared', this.onDialogAppeared) + } + + private onDialogAppeared = () => { + requestAnimationFrame(this.onResized) } private onSwapDoc = (cm: Editor, oldDoc: Doc) => { @@ -199,6 +205,7 @@ export class CodeMirrorHost extends React.Component { } this.resizeObserver.disconnect() + document.removeEventListener('dialog-show', this.onDialogAppeared) } public componentDidUpdate(prevProps: ICodeMirrorHostProps) { From 7bd2924c0ff75bafb52335e20d1e54ae7c607970 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Fri, 21 Oct 2022 10:35:00 -0400 Subject: [PATCH 125/262] Centralize popup manager to be accessed through the app-store --- app/src/lib/app-state.ts | 2 ++ app/src/lib/menu-update.ts | 3 +- app/src/lib/multi-commit-operation.ts | 5 ++- app/src/lib/popup-manager.ts | 8 ++--- app/src/lib/stores/app-store.ts | 47 ++++++++++++++++++--------- app/src/ui/app.tsx | 5 ++- app/src/ui/index.tsx | 5 ++- 7 files changed, 43 insertions(+), 32 deletions(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index 952da275d1..bd996cb09f 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -46,6 +46,7 @@ import { MultiCommitOperationStep, } from '../models/multi-commit-operation' import { IChangesetData } from './git' +import { Popup } from '../models/popup' export enum SelectionType { Repository, @@ -114,6 +115,7 @@ export interface IAppState { readonly showWelcomeFlow: boolean readonly focusCommitMessage: boolean + readonly currentPopup: Popup | null readonly currentFoldout: Foldout | null readonly currentBanner: Banner | null diff --git a/app/src/lib/menu-update.ts b/app/src/lib/menu-update.ts index c4825b0de1..9f0c644679 100644 --- a/app/src/lib/menu-update.ts +++ b/app/src/lib/menu-update.ts @@ -11,7 +11,6 @@ import { updateMenuState as ipcUpdateMenuState } from '../ui/main-process-proxy' import { AppMenu, MenuItem } from '../models/app-menu' import { hasConflictedFiles } from './status' import { findContributionTargetDefaultBranch } from './branch' -import { popupManager } from './popup-manager' export interface IMenuItemState { readonly enabled?: boolean @@ -364,7 +363,7 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder { } function getMenuState(state: IAppState): Map { - if (popupManager.isAPopupOpen) { + if (state.currentPopup) { return getAllMenusDisabledBuilder().state } diff --git a/app/src/lib/multi-commit-operation.ts b/app/src/lib/multi-commit-operation.ts index 8694cdf0ae..c1799a1dd1 100644 --- a/app/src/lib/multi-commit-operation.ts +++ b/app/src/lib/multi-commit-operation.ts @@ -4,10 +4,8 @@ import { conflictSteps, MultiCommitOperationStepKind, } from '../models/multi-commit-operation' -import { PopupType } from '../models/popup' import { TipState } from '../models/tip' import { IMultiCommitOperationState, IRepositoryState } from './app-state' -import { popupManager } from './popup-manager' /** * Setup the multi commit operation state when the user needs to select a branch as the @@ -40,10 +38,11 @@ export function getMultiCommitOperationChooseBranchStep( } export function isConflictsFlow( + isMultiCommitOperationPopupOpen: boolean, multiCommitOperationState: IMultiCommitOperationState | null ): boolean { return ( - popupManager.arePopupsOfType(PopupType.MultiCommitOperation) && + isMultiCommitOperationPopupOpen && multiCommitOperationState !== null && conflictSteps.includes(multiCommitOperationState.step.kind) ) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index f2f7ce3047..97e611253b 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -18,15 +18,15 @@ export class PopupManager { /** * Returns the last popup added to the stack. */ - public get currentPopup(): Popup | undefined { - return this.popupStack.at(-1) + public get currentPopup(): Popup | null { + return this.popupStack.at(-1) ?? null } /** * Returns whether there are any popups in the stack. */ public get isAPopupOpen(): boolean { - return this.currentPopup !== undefined + return this.currentPopup !== null } /** @@ -116,5 +116,3 @@ export class PopupManager { this.popupStack = this.popupStack.filter(p => p.type !== popupType) } } - -export const popupManager = new PopupManager() diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 4aae35489d..5353a20a0a 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -304,7 +304,7 @@ import { offsetFromNow } from '../offset-from' import { findContributionTargetDefaultBranch } from '../branch' import { ValidNotificationPullRequestReview } from '../valid-notification-pull-request-review' import { determineMergeability } from '../git/merge-tree' -import { popupManager } from '../popup-manager' +import { PopupManager } from '../popup-manager' const LastSelectedRepositoryIDKey = 'last-selected-repository-id' @@ -500,6 +500,9 @@ export class AppStore extends TypedBaseStore { private lastThankYou: ILastThankYou | undefined private showCIStatusPopover: boolean = false + /** A service for managing the stack of open popups */ + private popupManager = new PopupManager() + public constructor( private readonly gitHubUserStore: GitHubUserStore, private readonly cloningRepositoriesStore: CloningRepositoriesStore, @@ -639,7 +642,7 @@ export class AppStore extends TypedBaseStore { // If there is a currently open popup, don't do anything here. Since the // app can only show one popup at a time, we don't want to close the current // one in favor of the error we're about to show. - if (popupManager.isAPopupOpen) { + if (this.popupManager.isAPopupOpen) { return } @@ -904,6 +907,7 @@ export class AppStore extends TypedBaseStore { appIsFocused: this.appIsFocused, selectedState: this.getSelectedState(), signInState: this.signInStore.getState(), + currentPopup: this.popupManager.currentPopup, currentFoldout: this.currentFoldout, errors: this.errors, showWelcomeFlow: this.showWelcomeFlow, @@ -2505,7 +2509,13 @@ export class AppStore extends TypedBaseStore { this.currentBanner !== null && this.currentBanner.type === BannerType.ConflictsFound - if (displayingBanner || isConflictsFlow(multiCommitOperationState)) { + if ( + displayingBanner || + isConflictsFlow( + this.popupManager.arePopupsOfType(PopupType.MultiCommitOperation), + multiCommitOperationState + ) + ) { return } @@ -2592,7 +2602,12 @@ export class AppStore extends TypedBaseStore { */ private clearConflictsFlowVisuals(state: IRepositoryState) { const { multiCommitOperationState } = state - if (userIsStartingMultiCommitOperation(multiCommitOperationState)) { + if ( + userIsStartingMultiCommitOperation( + this.popupManager.currentPopup, + multiCommitOperationState + ) + ) { return } @@ -3475,19 +3490,19 @@ export class AppStore extends TypedBaseStore { // applicable on Windows where we draw a custom app menu. this._closeFoldout(FoldoutType.AppMenu) - popupManager.addPopup(popup) + this.popupManager.addPopup(popup) this.emitUpdate() } /** This shouldn't be called directly. See `Dispatcher`. */ public _closePopup(popupType?: PopupType) { - const currentPopup = popupManager.currentPopup - if (currentPopup === undefined) { + const currentPopup = this.popupManager.currentPopup + if (currentPopup === null) { return } if (popupType === undefined) { - popupManager.removePopup(currentPopup) + this.popupManager.removePopup(currentPopup) } else { if (currentPopup.type !== popupType) { return @@ -3497,7 +3512,7 @@ export class AppStore extends TypedBaseStore { this._completeOpenInDesktop(() => Promise.resolve(null)) } - popupManager.removePopupByType(popupType) + this.popupManager.removePopupByType(popupType) } this.emitUpdate() @@ -6491,12 +6506,11 @@ export class AppStore extends TypedBaseStore { path, (title, value, description) => { if ( - popupManager.currentPopup !== undefined && - popupManager.currentPopup.type === - PopupType.CreateTutorialRepository + this.popupManager.currentPopup?.type === + PopupType.CreateTutorialRepository ) { - popupManager.updatePopup({ - ...popupManager.currentPopup, + this.popupManager.updatePopup({ + ...this.popupManager.currentPopup, progress: { kind: 'generic', title, value, description }, }) this.emitUpdate() @@ -7483,13 +7497,14 @@ function getInitialAction( } function userIsStartingMultiCommitOperation( + currentPopup: Popup | null, state: IMultiCommitOperationState | null ) { - if (!popupManager.isAPopupOpen || state === null) { + if (currentPopup === null || state === null) { return false } - if (popupManager.currentPopup?.type !== PopupType.MultiCommitOperation) { + if (currentPopup.type !== PopupType.MultiCommitOperation) { return false } diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index e8fe72a6e2..8a97914cbf 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -159,7 +159,6 @@ import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog' import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-dialog' import { sendNonFatalException } from '../lib/helpers/non-fatal-exception' import { createCommitURL } from '../lib/commit-url' -import { popupManager } from '../lib/popup-manager' const MinuteInMilliseconds = 1000 * 60 const HourInMilliseconds = MinuteInMilliseconds * 60 @@ -216,7 +215,7 @@ export class App extends React.Component { * modal dialog such as the preferences, or an error dialog. */ private get isShowingModal() { - return popupManager.isAPopupOpen || this.state.errors.length > 0 + return this.state.currentPopup !== null || this.state.errors.length > 0 } /** @@ -1376,7 +1375,7 @@ export class App extends React.Component { return null } - const popup = popupManager.currentPopup + const popup = this.state.currentPopup if (!popup) { return null diff --git a/app/src/ui/index.tsx b/app/src/ui/index.tsx index 8f037dcfc9..2d883d5606 100644 --- a/app/src/ui/index.tsx +++ b/app/src/ui/index.tsx @@ -79,7 +79,6 @@ import { NotificationsStore } from '../lib/stores/notifications-store' import * as ipcRenderer from '../lib/ipc-renderer' import { migrateRendererGUID } from '../lib/get-renderer-guid' import { initializeRendererNotificationHandler } from '../lib/notifications/notification-handler' -import { popupManager } from '../lib/popup-manager' if (__DEV__) { installDevGlobals() @@ -145,8 +144,8 @@ const sendErrorWithContext = ( extra.currentBanner = currentState.currentBanner.type } - if (popupManager.currentPopup !== undefined) { - extra.currentPopup = `${popupManager.currentPopup.type}` + if (currentState.currentPopup !== null) { + extra.currentPopup = `${currentState.currentPopup.type}` } if (currentState.selectedState !== null) { From 27b29a0ea03afe549bafa0bc5fbf5ef02a799263 Mon Sep 17 00:00:00 2001 From: Shivareddy-Aluri Date: Mon, 24 Oct 2022 15:00:04 +0530 Subject: [PATCH 126/262] ability to copy tag names from the commit list. --- app/src/ui/history/commit-list-item.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/ui/history/commit-list-item.tsx b/app/src/ui/history/commit-list-item.tsx index dc4d4bb4be..c6acedad30 100644 --- a/app/src/ui/history/commit-list-item.tsx +++ b/app/src/ui/history/commit-list-item.tsx @@ -219,6 +219,10 @@ export class CommitListItem extends React.PureComponent< clipboard.writeText(this.props.commit.sha) } + private onCopyTags = () => { + clipboard.writeText(this.props.commit.tags.join(', ')) + } + private onViewOnGitHub = () => { if (this.props.onViewCommitOnGitHub) { this.props.onViewCommitOnGitHub(this.props.commit.sha) @@ -353,6 +357,11 @@ export class CommitListItem extends React.PureComponent< label: 'Copy SHA', action: this.onCopySHA, }, + { + label: 'Copy Tags', + action: this.onCopyTags, + enabled: this.props.commit.tags.length > 0 + }, { label: viewOnGitHubLabel, action: this.onViewOnGitHub, From 53cc945f92df336895449ec17ba892b98016e89a Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 06:03:31 -0400 Subject: [PATCH 127/262] No need for array copy. Co-authored-by: Sergio Padrino --- app/src/lib/popup-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 97e611253b..095e460d97 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -33,7 +33,7 @@ export class PopupManager { * Returns an array of all popups in the stack of the provided type. **/ public getPopupsOfType(popupType: PopupType): ReadonlyArray { - return [...this.popupStack.filter(p => p.type === popupType)] + return this.popupStack.filter(p => p.type === popupType) } /** From 3cc3b8814887a66078b82f3753339f6274c69849 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 06:08:50 -0400 Subject: [PATCH 128/262] Should be checking against undefined Co-authored-by: Sergio Padrino --- app/src/lib/popup-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 095e460d97..fcd1c399a7 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -102,7 +102,7 @@ export class PopupManager { * Removes a popup based on it's id. */ public removePopup(popup: Popup) { - if (popup.id === null) { + if (popup.id === undefined) { log.warn(`Attempted to remove a popup without an id.`) return } From 4a4ef96015981bc5075bcf62aa2e740fc1cbb9ba Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 06:10:20 -0400 Subject: [PATCH 129/262] Are there or are? Co-Authored-By: Sergio Padrino <1083228+sergiou87@users.noreply.github.com> --- app/src/lib/popup-manager.ts | 2 +- app/src/lib/stores/app-store.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 97e611253b..7a985ccae1 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -39,7 +39,7 @@ export class PopupManager { /** * Returns whether there are any popups of a given type in the stack. */ - public arePopupsOfType(popupType: PopupType): boolean { + public areTherePopupsOfType(popupType: PopupType): boolean { return this.popupStack.some(p => p.type === popupType) } diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 5353a20a0a..6e37e97476 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -2512,7 +2512,7 @@ export class AppStore extends TypedBaseStore { if ( displayingBanner || isConflictsFlow( - this.popupManager.arePopupsOfType(PopupType.MultiCommitOperation), + this.popupManager.areTherePopupsOfType(PopupType.MultiCommitOperation), multiCommitOperationState ) ) { From ae91f5acfd30c622a6d53a2d81c26e48c720ced8 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 06:11:53 -0400 Subject: [PATCH 130/262] Less changes same extensibility This reverts commit ba0cbcde3efc5df08675378402cf767f3361b1d8. Co-Authored-By: Sergio Padrino <1083228+sergiou87@users.noreply.github.com> --- app/src/models/popup.ts | 738 ++++++++++++++++------------------------ 1 file changed, 284 insertions(+), 454 deletions(-) diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 3bdab997a5..55b0d31b48 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -96,458 +96,288 @@ interface IBasePopup { readonly id?: string } -interface IRenameBranchPopup extends IBasePopup { - type: PopupType.RenameBranch - repository: Repository - branch: Branch -} +export type PopupDetail = + | { type: PopupType.RenameBranch; repository: Repository; branch: Branch } + | { + type: PopupType.DeleteBranch + repository: Repository + branch: Branch + existsOnRemote: boolean + } + | { + type: PopupType.DeleteRemoteBranch + repository: Repository + branch: Branch + } + | { + type: PopupType.ConfirmDiscardChanges + repository: Repository + files: ReadonlyArray + showDiscardChangesSetting?: boolean + discardingAllChanges?: boolean + } + | { + type: PopupType.ConfirmDiscardSelection + repository: Repository + file: WorkingDirectoryFileChange + diff: ITextDiff + selection: DiffSelection + } + | { type: PopupType.Preferences; initialSelectedTab?: PreferencesTab } + | { + type: PopupType.RepositorySettings + repository: Repository + initialSelectedTab?: RepositorySettingsTab + } + | { type: PopupType.AddRepository; path?: string } + | { type: PopupType.CreateRepository; path?: string } + | { + type: PopupType.CloneRepository + initialURL: string | null + } + | { + type: PopupType.CreateBranch + repository: Repository + initialName?: string + targetCommit?: CommitOneLine + } + | { type: PopupType.SignIn } + | { type: PopupType.About } + | { type: PopupType.InstallGit; path: string } + | { type: PopupType.PublishRepository; repository: Repository } + | { type: PopupType.Acknowledgements } + | { + type: PopupType.UntrustedCertificate + certificate: Electron.Certificate + url: string + } + | { type: PopupType.RemoveRepository; repository: Repository } + | { type: PopupType.TermsAndConditions } + | { + type: PopupType.PushBranchCommits + repository: Repository + branch: Branch + unPushedCommits?: number + } + | { type: PopupType.CLIInstalled } + | { + type: PopupType.GenericGitAuthentication + hostname: string + retryAction: RetryAction + } + | { + type: PopupType.ExternalEditorFailed + message: string + suggestDefaultEditor?: boolean + openPreferences?: boolean + } + | { type: PopupType.OpenShellFailed; message: string } + | { type: PopupType.InitializeLFS; repositories: ReadonlyArray } + | { type: PopupType.LFSAttributeMismatch } + | { + type: PopupType.UpstreamAlreadyExists + repository: Repository + existingRemote: IRemote + } + | { + type: PopupType.ReleaseNotes + newReleases: ReadonlyArray + } + | { + type: PopupType.DeletePullRequest + repository: Repository + branch: Branch + pullRequest: PullRequest + } + | { + type: PopupType.OversizedFiles + oversizedFiles: ReadonlyArray + context: ICommitContext + repository: Repository + } + | { + type: PopupType.CommitConflictsWarning + /** files that were selected for committing that are also conflicted */ + files: ReadonlyArray + /** repository user is committing in */ + repository: Repository + /** information for completing the commit */ + context: ICommitContext + } + | { + type: PopupType.PushNeedsPull + repository: Repository + } + | { + type: PopupType.ConfirmForcePush + repository: Repository + upstreamBranch: string + } + | { + type: PopupType.StashAndSwitchBranch + repository: Repository + branchToCheckout: Branch + } + | { + type: PopupType.ConfirmOverwriteStash + repository: Repository + branchToCheckout: Branch | null + } + | { + type: PopupType.ConfirmDiscardStash + repository: Repository + stash: IStashEntry + } + | { + type: PopupType.CreateTutorialRepository + account: Account + progress?: Progress + } + | { + type: PopupType.ConfirmExitTutorial + } + | { + type: PopupType.PushRejectedDueToMissingWorkflowScope + rejectedPath: string + repository: RepositoryWithGitHubRepository + } + | { + type: PopupType.SAMLReauthRequired + organizationName: string + endpoint: string + retryAction?: RetryAction + } + | { + type: PopupType.CreateFork + repository: RepositoryWithGitHubRepository + account: Account + } + | { + type: PopupType.CreateTag + repository: Repository + targetCommitSha: string + initialName?: string + localTags: Map | null + } + | { + type: PopupType.DeleteTag + repository: Repository + tagName: string + } + | { + type: PopupType.ChooseForkSettings + repository: RepositoryWithForkedGitHubRepository + } + | { + type: PopupType.LocalChangesOverwritten + repository: Repository + retryAction: RetryAction + files: ReadonlyArray + } + | { type: PopupType.MoveToApplicationsFolder } + | { type: PopupType.ChangeRepositoryAlias; repository: Repository } + | { + type: PopupType.ThankYou + userContributions: ReadonlyArray + friendlyName: string + latestVersion: string | null + } + | { + type: PopupType.CommitMessage + coAuthors: ReadonlyArray + showCoAuthoredBy: boolean + commitMessage: ICommitMessage | null + dialogTitle: string + dialogButtonText: string + prepopulateCommitSummary: boolean + repository: Repository + onSubmitCommitMessage: (context: ICommitContext) => Promise + } + | { + type: PopupType.MultiCommitOperation + repository: Repository + } + | { + type: PopupType.WarnLocalChangesBeforeUndo + repository: Repository + commit: Commit + isWorkingDirectoryClean: boolean + } + | { + type: PopupType.WarningBeforeReset + repository: Repository + commit: Commit + } + | { + type: PopupType.InvalidatedToken + account: Account + } + | { + type: PopupType.AddSSHHost + host: string + ip: string + keyType: string + fingerprint: string + onSubmit: (addHost: boolean) => void + } + | { + type: PopupType.SSHKeyPassphrase + keyPath: string + onSubmit: ( + passphrase: string | undefined, + storePassphrase: boolean + ) => void + } + | { + type: PopupType.SSHUserPassword + username: string + onSubmit: (password: string | undefined, storePassword: boolean) => void + } + | { + type: PopupType.PullRequestChecksFailed + repository: RepositoryWithGitHubRepository + pullRequest: PullRequest + shouldChangeRepository: boolean + commitMessage: string + commitSha: string + checks: ReadonlyArray + } + | { + type: PopupType.CICheckRunRerun + checkRuns: ReadonlyArray + repository: GitHubRepository + prRef: string + failedOnly: boolean + } + | { type: PopupType.WarnForcePush; operation: string; onBegin: () => void } + | { + type: PopupType.DiscardChangesRetry + retryAction: RetryAction + } + | { + type: PopupType.PullRequestReview + repository: RepositoryWithGitHubRepository + pullRequest: PullRequest + review: ValidNotificationPullRequestReview + numberOfComments: number + shouldCheckoutBranch: boolean + shouldChangeRepository: boolean + } + | { + type: PopupType.UnreachableCommits + selectedTab: UnreachableCommitsTab + } + | { + type: PopupType.StartPullRequest + allBranches: ReadonlyArray + currentBranch: Branch + defaultBranch: Branch | null + externalEditorLabel?: string + imageDiffType: ImageDiffType + recentBranches: ReadonlyArray + repository: Repository + nonLocalCommitSHA: string | null + showSideBySideDiff: boolean + } -interface IDeleteBranchPopup extends IBasePopup { - type: PopupType.DeleteBranch - repository: Repository - branch: Branch - existsOnRemote: boolean -} - -interface IDeleteRemoteBranchPopup extends IBasePopup { - type: PopupType.DeleteRemoteBranch - repository: Repository - branch: Branch -} - -interface IConfirmDiscardChangesPopup extends IBasePopup { - type: PopupType.ConfirmDiscardChanges - repository: Repository - files: ReadonlyArray - showDiscardChangesSetting?: boolean - discardingAllChanges?: boolean -} - -interface IConfirmDiscardSelectionPopup extends IBasePopup { - type: PopupType.ConfirmDiscardSelection - repository: Repository - file: WorkingDirectoryFileChange - diff: ITextDiff - selection: DiffSelection -} - -interface IPreferencesPopup extends IBasePopup { - type: PopupType.Preferences - initialSelectedTab?: PreferencesTab -} - -interface IRepositorySettingsPopup extends IBasePopup { - type: PopupType.RepositorySettings - repository: Repository - initialSelectedTab?: RepositorySettingsTab -} - -interface IAddRepositoryPopup extends IBasePopup { - type: PopupType.AddRepository - path?: string -} - -interface ICreateRepositoryPopup extends IBasePopup { - type: PopupType.CreateRepository - path?: string -} - -interface ICloneRepositoryPopup extends IBasePopup { - type: PopupType.CloneRepository - initialURL: string | null -} - -interface ICreateBranchPopup extends IBasePopup { - type: PopupType.CreateBranch - repository: Repository - initialName?: string - targetCommit?: CommitOneLine -} - -interface ISignInPopup extends IBasePopup { - type: PopupType.SignIn -} - -interface IAboutPopup extends IBasePopup { - type: PopupType.About -} - -interface IInstallGitPopup extends IBasePopup { - type: PopupType.InstallGit - path: string -} - -interface IPublishRepositoryPopup extends IBasePopup { - type: PopupType.PublishRepository - repository: Repository -} - -interface IAcknowledgementsPopup extends IBasePopup { - type: PopupType.Acknowledgements -} - -interface IUntrustedCertificatePopup extends IBasePopup { - type: PopupType.UntrustedCertificate - certificate: Electron.Certificate - url: string -} - -interface IRemoveRepositoryPopup extends IBasePopup { - type: PopupType.RemoveRepository - repository: Repository -} - -interface ITermsAndConditionsPopup extends IBasePopup { - type: PopupType.TermsAndConditions -} - -interface IPushBranchCommitsPopup extends IBasePopup { - type: PopupType.PushBranchCommits - repository: Repository - branch: Branch - unPushedCommits?: number -} - -interface ICLIInstalledPopup extends IBasePopup { - type: PopupType.CLIInstalled -} - -interface IGenericGitAuthenticationPopup extends IBasePopup { - type: PopupType.GenericGitAuthentication - hostname: string - retryAction: RetryAction -} - -interface IExternalEditorFailedPopup extends IBasePopup { - type: PopupType.ExternalEditorFailed - message: string - suggestDefaultEditor?: boolean - openPreferences?: boolean -} - -interface IOpenShellFailedPopup extends IBasePopup { - type: PopupType.OpenShellFailed - message: string -} - -interface IInitializeLFSPopup extends IBasePopup { - type: PopupType.InitializeLFS - repositories: ReadonlyArray -} - -interface ILFSAttributeMismatchPopup extends IBasePopup { - type: PopupType.LFSAttributeMismatch -} - -interface IUpstreamAlreadyExistsPopup extends IBasePopup { - type: PopupType.UpstreamAlreadyExists - repository: Repository - existingRemote: IRemote -} - -interface IReleaseNotesPopup extends IBasePopup { - type: PopupType.ReleaseNotes - newReleases: ReadonlyArray -} - -interface IDeletePullRequestPopup extends IBasePopup { - type: PopupType.DeletePullRequest - repository: Repository - branch: Branch - pullRequest: PullRequest -} - -interface IOversizedFilesPopup extends IBasePopup { - type: PopupType.OversizedFiles - oversizedFiles: ReadonlyArray - context: ICommitContext - repository: Repository -} - -interface ICommitConflictsWarningPopup extends IBasePopup { - type: PopupType.CommitConflictsWarning - /** files that were selected for committing that are also conflicted */ - files: ReadonlyArray - /** repository user is committing in */ - repository: Repository - /** information for completing the commit */ - context: ICommitContext -} - -interface IPushNeedsPullPopup extends IBasePopup { - type: PopupType.PushNeedsPull - repository: Repository -} - -interface IConfirmForcePushPopup extends IBasePopup { - type: PopupType.ConfirmForcePush - repository: Repository - upstreamBranch: string -} - -interface IStashAndSwitchBranchPopup extends IBasePopup { - type: PopupType.StashAndSwitchBranch - repository: Repository - branchToCheckout: Branch -} - -interface IConfirmOverwriteStashPopup extends IBasePopup { - type: PopupType.ConfirmOverwriteStash - repository: Repository - branchToCheckout: Branch | null -} - -interface IConfirmDiscardStashPopup extends IBasePopup { - type: PopupType.ConfirmDiscardStash - repository: Repository - stash: IStashEntry -} - -interface ICreateTutorialRepositoryPopup extends IBasePopup { - type: PopupType.CreateTutorialRepository - account: Account - progress?: Progress -} - -interface IConfirmExitTutorialPopup extends IBasePopup { - type: PopupType.ConfirmExitTutorial -} - -interface IPushRejectedDueToMissingWorkflowScopePopup extends IBasePopup { - type: PopupType.PushRejectedDueToMissingWorkflowScope - rejectedPath: string - repository: RepositoryWithGitHubRepository -} - -interface ISAMLReauthRequiredPopup extends IBasePopup { - type: PopupType.SAMLReauthRequired - organizationName: string - endpoint: string - retryAction?: RetryAction -} - -interface ICreateForkPopup extends IBasePopup { - type: PopupType.CreateFork - repository: RepositoryWithGitHubRepository - account: Account -} - -interface ICreateTagPopup extends IBasePopup { - type: PopupType.CreateTag - repository: Repository - targetCommitSha: string - initialName?: string - localTags: Map | null -} - -interface IDeleteTagPopup extends IBasePopup { - type: PopupType.DeleteTag - repository: Repository - tagName: string -} - -interface IChooseForkSettingsPopup extends IBasePopup { - type: PopupType.ChooseForkSettings - repository: RepositoryWithForkedGitHubRepository -} - -interface ILocalChangesOverwrittenPopup extends IBasePopup { - type: PopupType.LocalChangesOverwritten - repository: Repository - retryAction: RetryAction - files: ReadonlyArray -} - -interface IMoveToApplicationsFolderPopup extends IBasePopup { - type: PopupType.MoveToApplicationsFolder -} - -interface IChangeRepositoryAliasPopup extends IBasePopup { - type: PopupType.ChangeRepositoryAlias - repository: Repository -} - -interface IThankYouPopup extends IBasePopup { - type: PopupType.ThankYou - userContributions: ReadonlyArray - friendlyName: string - latestVersion: string | null -} - -interface ICommitMessagePopup extends IBasePopup { - type: PopupType.CommitMessage - coAuthors: ReadonlyArray - showCoAuthoredBy: boolean - commitMessage: ICommitMessage | null - dialogTitle: string - dialogButtonText: string - prepopulateCommitSummary: boolean - repository: Repository - onSubmitCommitMessage: (context: ICommitContext) => Promise -} - -interface IMultiCommitOperationPopup extends IBasePopup { - type: PopupType.MultiCommitOperation - repository: Repository -} - -interface IWarnLocalChangesBeforeUndoPopup extends IBasePopup { - type: PopupType.WarnLocalChangesBeforeUndo - repository: Repository - commit: Commit - isWorkingDirectoryClean: boolean -} - -interface IWarningBeforeResetPopup extends IBasePopup { - type: PopupType.WarningBeforeReset - repository: Repository - commit: Commit -} - -interface IInvalidatedTokenPopup extends IBasePopup { - type: PopupType.InvalidatedToken - account: Account -} - -interface IAddSSHHostPopup extends IBasePopup { - type: PopupType.AddSSHHost - host: string - ip: string - keyType: string - fingerprint: string - onSubmit: (addHost: boolean) => void -} - -interface ISSHKeyPassphrasePopup extends IBasePopup { - type: PopupType.SSHKeyPassphrase - keyPath: string - onSubmit: (passphrase: string | undefined, storePassphrase: boolean) => void -} - -interface ISSHUserPasswordPopup extends IBasePopup { - type: PopupType.SSHUserPassword - username: string - onSubmit: (password: string | undefined, storePassword: boolean) => void -} - -interface IPullRequestChecksFailedPopup extends IBasePopup { - type: PopupType.PullRequestChecksFailed - repository: RepositoryWithGitHubRepository - pullRequest: PullRequest - shouldChangeRepository: boolean - commitMessage: string - commitSha: string - checks: ReadonlyArray -} - -interface ICICheckRunRerunPopup extends IBasePopup { - type: PopupType.CICheckRunRerun - checkRuns: ReadonlyArray - repository: GitHubRepository - prRef: string - failedOnly: boolean -} - -interface IWarnForcePushPopup extends IBasePopup { - type: PopupType.WarnForcePush - operation: string - onBegin: () => void -} - -interface IDiscardChangesRetryPopup extends IBasePopup { - type: PopupType.DiscardChangesRetry - retryAction: RetryAction -} - -interface IPullRequestReviewPopup extends IBasePopup { - type: PopupType.PullRequestReview - repository: RepositoryWithGitHubRepository - pullRequest: PullRequest - review: ValidNotificationPullRequestReview - numberOfComments: number - shouldCheckoutBranch: boolean - shouldChangeRepository: boolean -} - -interface IUnreachableCommitsPopup extends IBasePopup { - type: PopupType.UnreachableCommits - selectedTab: UnreachableCommitsTab -} - -interface IStartPullRequestPopup extends IBasePopup { - type: PopupType.StartPullRequest - allBranches: ReadonlyArray - currentBranch: Branch - defaultBranch: Branch | null - externalEditorLabel?: string - imageDiffType: ImageDiffType - recentBranches: ReadonlyArray - repository: Repository - nonLocalCommitSHA: string | null - showSideBySideDiff: boolean -} - -export type Popup = - | IRenameBranchPopup - | IDeleteBranchPopup - | IDeleteRemoteBranchPopup - | IConfirmDiscardChangesPopup - | IConfirmDiscardSelectionPopup - | IPreferencesPopup - | IRepositorySettingsPopup - | IAddRepositoryPopup - | ICreateRepositoryPopup - | ICloneRepositoryPopup - | ICreateBranchPopup - | ISignInPopup - | IAboutPopup - | IInstallGitPopup - | IPublishRepositoryPopup - | IAcknowledgementsPopup - | IUntrustedCertificatePopup - | IRemoveRepositoryPopup - | ITermsAndConditionsPopup - | IPushBranchCommitsPopup - | ICLIInstalledPopup - | IGenericGitAuthenticationPopup - | IExternalEditorFailedPopup - | IOpenShellFailedPopup - | IInitializeLFSPopup - | ILFSAttributeMismatchPopup - | IUpstreamAlreadyExistsPopup - | IReleaseNotesPopup - | IDeletePullRequestPopup - | IOversizedFilesPopup - | ICommitConflictsWarningPopup - | IPushNeedsPullPopup - | IConfirmForcePushPopup - | IStashAndSwitchBranchPopup - | IConfirmOverwriteStashPopup - | IConfirmDiscardStashPopup - | ICreateTutorialRepositoryPopup - | IConfirmExitTutorialPopup - | IPushRejectedDueToMissingWorkflowScopePopup - | ISAMLReauthRequiredPopup - | ICreateForkPopup - | ICreateTagPopup - | IDeleteTagPopup - | IChooseForkSettingsPopup - | ILocalChangesOverwrittenPopup - | IMoveToApplicationsFolderPopup - | IChangeRepositoryAliasPopup - | IThankYouPopup - | ICommitMessagePopup - | IMultiCommitOperationPopup - | IWarnLocalChangesBeforeUndoPopup - | IWarningBeforeResetPopup - | IInvalidatedTokenPopup - | IAddSSHHostPopup - | ISSHKeyPassphrasePopup - | ISSHUserPasswordPopup - | IPullRequestChecksFailedPopup - | ICICheckRunRerunPopup - | IWarnForcePushPopup - | IDiscardChangesRetryPopup - | IPullRequestReviewPopup - | IUnreachableCommitsPopup - | IStartPullRequestPopup +export type Popup = IBasePopup & PopupDetail From 936120fa1495b84b21fc6b2e879c2dce382a2b90 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 06:32:52 -0400 Subject: [PATCH 131/262] Feature-flag it Co-Authored-By: Sergio Padrino <1083228+sergiou87@users.noreply.github.com> --- app/src/lib/feature-flag.ts | 5 +++++ app/src/lib/popup-manager.ts | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/app/src/lib/feature-flag.ts b/app/src/lib/feature-flag.ts index f411e68b1a..51540db53b 100644 --- a/app/src/lib/feature-flag.ts +++ b/app/src/lib/feature-flag.ts @@ -112,3 +112,8 @@ export function enableSubmoduleDiff(): boolean { export function enableStartingPullRequests(): boolean { return enableBetaFeatures() } + +/** Should we enable starting pull requests? */ +export function enableStackedPopups(): boolean { + return enableDevelopmentFeatures() +} diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index f7c4908307..5a3ab5c493 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -1,5 +1,6 @@ import uuid from 'uuid' import { Popup, PopupType } from '../models/popup' +import { enableStackedPopups } from './feature-flag' import { sendNonFatalException } from './helpers/non-fatal-exception' /** @@ -50,6 +51,12 @@ export class PopupManager { **/ public addPopup(popupToAdd: Popup): Popup { const existingPopup = this.getPopupsOfType(popupToAdd.type) + + if (!enableStackedPopups()) { + this.popupStack = [popupToAdd] + return popupToAdd + } + if (existingPopup.length > 0) { log.warn( `Attempted to add a popup of already existing type - ${popupToAdd.type}.` From d9a09f897a91c9dd16e9c180731eae3c9376f629 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 07:51:16 -0400 Subject: [PATCH 132/262] Test stacked popups --- app/src/lib/popup-manager.ts | 17 +- app/test/unit/popup-manager-test.ts | 251 ++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 app/test/unit/popup-manager-test.ts diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 5a3ab5c493..e5a9346a8f 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -1,14 +1,14 @@ -import uuid from 'uuid' 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 popupStackLimit = 50 +const defaultPopupStackLimit = 50 /** * The popup manager is to manage the stack of currently open popups. @@ -16,6 +16,8 @@ const popupStackLimit = 50 export class PopupManager { private popupStack = new Array() + public constructor(private readonly popupLimit = defaultPopupStackLimit) {} + /** * Returns the last popup added to the stack. */ @@ -67,13 +69,13 @@ export class PopupManager { const popup = { id: uuid(), ...popupToAdd } this.popupStack.push(popup) - if (this.popupStack.length > popupStackLimit) { + if (this.popupStack.length > this.popupLimit) { // Remove the oldest const oldest = this.popupStack[0] sendNonFatalException( 'TooManyPopups', new Error( - `Max number of ${popupStackLimit} popups reached while adding popup of type ${popup.type}. Removing last popup from the stack -> type ${oldest.type} ` + `Max number of ${this.popupLimit} popups reached while adding popup of type ${popup.type}. Removing last popup from the stack -> type ${oldest.type} ` ) ) this.popupStack = this.popupStack.slice(1) @@ -85,16 +87,16 @@ export class PopupManager { * Updates a popup in the stack and returns it. * - It uses the popup id to find and update the popup. */ - public updatePopup(popupToUpdate: Popup): Popup { + public updatePopup(popupToUpdate: Popup) { if (popupToUpdate.id === undefined) { log.warn(`Attempted to update a popup without an id.`) - return popupToUpdate + 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 popupToUpdate + return } this.popupStack = [ @@ -102,7 +104,6 @@ export class PopupManager { popupToUpdate, ...this.popupStack.slice(index + 1), ] - return popupToUpdate } /** diff --git a/app/test/unit/popup-manager-test.ts b/app/test/unit/popup-manager-test.ts new file mode 100644 index 0000000000..4d89919ffd --- /dev/null +++ b/app/test/unit/popup-manager-test.ts @@ -0,0 +1,251 @@ +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 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) + }) + }) + + 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('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('returns 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) + }) + }) +}) From ab2f0bf2bcd249f91e65591a6bc78d1acf2cb953 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 08:50:53 -0400 Subject: [PATCH 133/262] Only listen for dialog events when code mirror is descendent of a dialog Co-Authored-By: Sergio Padrino <1083228+sergiou87@users.noreply.github.com> --- app/src/ui/diff/code-mirror-host.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/ui/diff/code-mirror-host.tsx b/app/src/ui/diff/code-mirror-host.tsx index ab018444cf..80d7e04566 100644 --- a/app/src/ui/diff/code-mirror-host.tsx +++ b/app/src/ui/diff/code-mirror-host.tsx @@ -165,7 +165,9 @@ export class CodeMirrorHost extends React.Component { CodeMirrorHost.updateDoc(this.codeMirror, this.props.value) this.resizeObserver.observe(this.codeMirror.getWrapperElement()) - document.addEventListener('dialog-appeared', this.onDialogAppeared) + if (this.wrapper !== null && this.wrapper.closest('dialog') !== null) { + document.addEventListener('dialog-appeared', this.onDialogAppeared) + } } private onDialogAppeared = () => { From 35ec067c773e8c2cbd6b5669b6c8a5c5c073cf8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:48:14 +0000 Subject: [PATCH 134/262] Bump peter-evans/create-pull-request from 4.1.4 to 4.2.0 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.1.4 to 4.2.0. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v4.1.4...v4.2.0) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index c9a249428b..944398dcb3 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -37,7 +37,7 @@ jobs: private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} - name: Create Release Pull Request - uses: peter-evans/create-pull-request@v4.1.4 + uses: peter-evans/create-pull-request@v4.2.0 if: | startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test') with: From e0bf0888c9ab4ae9a28c1005036c47b97a76e373 Mon Sep 17 00:00:00 2001 From: Angus Date: Mon, 24 Oct 2022 18:29:45 +0100 Subject: [PATCH 135/262] Close repository list after create / add repository --- app/src/ui/add-repository/add-existing-repository.tsx | 2 ++ app/src/ui/add-repository/create-repository.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/ui/add-repository/add-existing-repository.tsx b/app/src/ui/add-repository/add-existing-repository.tsx index 1084a2d578..bc0cba7efc 100644 --- a/app/src/ui/add-repository/add-existing-repository.tsx +++ b/app/src/ui/add-repository/add-existing-repository.tsx @@ -11,6 +11,7 @@ import * as OcticonSymbol from '../octicons/octicons.generated' import { LinkButton } from '../lib/link-button' import { PopupType } from '../../models/popup' import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' +import { FoldoutType } from '../../lib/app-state' import untildify from 'untildify' import { showOpenDialog } from '../main-process-proxy' @@ -265,6 +266,7 @@ export class AddExistingRepository extends React.Component< const repositories = await dispatcher.addRepositories([resolvedPath]) if (repositories.length > 0) { + this.props.dispatcher.closeFoldout(FoldoutType.Repository) dispatcher.selectRepository(repositories[0]) dispatcher.recordAddExistingRepository() } diff --git a/app/src/ui/add-repository/create-repository.tsx b/app/src/ui/add-repository/create-repository.tsx index 08855c642a..d7a3846db8 100644 --- a/app/src/ui/add-repository/create-repository.tsx +++ b/app/src/ui/add-repository/create-repository.tsx @@ -34,6 +34,7 @@ import { showOpenDialog } from '../main-process-proxy' import { pathExists } from '../lib/path-exists' import { mkdir } from 'fs/promises' import { directoryExists } from '../../lib/directory-exists' +import { FoldoutType } from '../../lib/app-state' import { join } from 'path' /** The sentinel value used to indicate no gitignore should be used. */ @@ -271,6 +272,7 @@ export class CreateRepository extends React.Component< this.setState({ creating: true }) + this.props.dispatcher.closeFoldout(FoldoutType.Repository) try { await initGitRepository(fullPath) } catch (e) { From f97ca0b321d4237e26db908e004f0ee19afd835d Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:23:52 -0400 Subject: [PATCH 136/262] Add dev option to show an error --- app/src/main-process/menu/build-default-menu.ts | 4 ++++ app/src/main-process/menu/menu-event.ts | 1 + app/src/ui/app.tsx | 7 ++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main-process/menu/build-default-menu.ts b/app/src/main-process/menu/build-default-menu.ts index 5da9141700..b5e456be5b 100644 --- a/app/src/main-process/menu/build-default-menu.ts +++ b/app/src/main-process/menu/build-default-menu.ts @@ -551,6 +551,10 @@ export function buildDefaultMenu({ label: 'Pull Request Check Run Failed', click: emit('pull-request-check-run-failed'), }, + { + label: 'Show App Error', + click: emit('show-app-error'), + }, ], }, { diff --git a/app/src/main-process/menu/menu-event.ts b/app/src/main-process/menu/menu-event.ts index 235ff73e2b..a0e62eb06b 100644 --- a/app/src/main-process/menu/menu-event.ts +++ b/app/src/main-process/menu/menu-event.ts @@ -43,3 +43,4 @@ export type MenuEvent = | 'create-issue-in-repository-on-github' | 'pull-request-check-run-failed' | 'start-pull-request' + | 'show-app-error' diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 8a97914cbf..2f8af1f825 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -159,6 +159,7 @@ import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog' import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-dialog' import { sendNonFatalException } from '../lib/helpers/non-fatal-exception' import { createCommitURL } from '../lib/commit-url' +import { uuid } from '../lib/uuid' const MinuteInMilliseconds = 1000 * 60 const HourInMilliseconds = MinuteInMilliseconds * 60 @@ -347,7 +348,7 @@ export class App extends React.Component { private onMenuEvent(name: MenuEvent): any { // Don't react to menu events when an error dialog is shown. - if (this.state.errors.length) { + if (name !== 'show-app-error' && this.state.errors.length) { return } @@ -444,6 +445,10 @@ export class App extends React.Component { return this.findText() case 'pull-request-check-run-failed': return this.testPullRequestCheckRunFailed() + case 'show-app-error': + return this.props.dispatcher.postError( + new Error('Test Error - to use default error handler' + uuid()) + ) default: return assertNever(name, `Unknown menu event name: ${name}`) } From 3a6a3e070adcb77a862df9aff2262c1249621ec1 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:37:45 -0400 Subject: [PATCH 137/262] Move AppError into Popup model --- app/src/models/popup.ts | 5 +++++ app/src/ui/app.tsx | 29 +++++++++++------------------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 55b0d31b48..d5573e65f1 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -87,6 +87,7 @@ export enum PopupType { PullRequestReview = 'PullRequestReview', UnreachableCommits = 'UnreachableCommits', StartPullRequest = 'StartPullRequest', + Error = 'Error', } interface IBasePopup { @@ -379,5 +380,9 @@ export type PopupDetail = nonLocalCommitSHA: string | null showSideBySideDiff: boolean } + | { + type: PopupType.Error + error: Error + } export type Popup = IBasePopup & PopupDetail diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 2f8af1f825..286d2a5143 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -216,7 +216,7 @@ export class App extends React.Component { * modal dialog such as the preferences, or an error dialog. */ private get isShowingModal() { - return this.state.currentPopup !== null || this.state.errors.length > 0 + return this.state.currentPopup !== null } /** @@ -1375,11 +1375,6 @@ export class App extends React.Component { this.props.dispatcher.setUpdateBannerVisibility(false) private currentPopupContent(): JSX.Element | null { - // Hide any dialogs while we're displaying an error - if (this.state.errors.length) { - return null - } - const popup = this.state.currentPopup if (!popup) { @@ -2303,6 +2298,16 @@ export class App extends React.Component { /> ) } + case PopupType.Error: { + return ( + + ) + } default: return assertNever(popup, `Unknown popup type: ${popup}`) } @@ -2478,17 +2483,6 @@ export class App extends React.Component { this.props.dispatcher.setConfirmDiscardChangesPermanentlySetting(value) } - private renderAppError() { - return ( - - ) - } - private onRetryAction = (retryAction: RetryAction) => { this.props.dispatcher.performRetry(retryAction) } @@ -2516,7 +2510,6 @@ export class App extends React.Component { {this.renderBanner()} {this.renderRepository()} {this.renderPopup()} - {this.renderAppError()} {this.renderDragElement()}

) From 95e51cd67eb41911a9c7f306f62038ebb5a83ecb Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:38:21 -0400 Subject: [PATCH 138/262] Add error management to popup manager --- app/src/lib/popup-manager.ts | 38 ++++++++++++++++++++++++++++----- app/src/lib/stores/app-store.ts | 2 ++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index e5a9346a8f..ed2450879c 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -19,10 +19,13 @@ export class PopupManager { public constructor(private readonly popupLimit = defaultPopupStackLimit) {} /** - * Returns the last popup added to the stack. + * Returns the last popup in the stack. + * 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 + const errorPopups = this.getPopupsOfType(PopupType.Error) + return errorPopups.at(-1) ?? this.popupStack.at(-1) ?? null } /** @@ -49,7 +52,8 @@ export class PopupManager { /** * 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 to the stack + * - 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 { const existingPopup = this.getPopupsOfType(popupToAdd.type) @@ -68,19 +72,34 @@ export class PopupManager { const popup = { id: uuid(), ...popupToAdd } this.popupStack.push(popup) + this.checkStackLength() + return popup + } + /* + * 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.push(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 ${popup.type}. Removing last popup from the stack -> type ${oldest.type} ` + `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) } - return popup } /** @@ -123,4 +142,13 @@ export class PopupManager { public removePopupByType(popupType: PopupType) { this.popupStack = this.popupStack.filter(p => p.type !== popupType) } + + /** + * Removes any popup with the given error + */ + public removeErrorPopup(error: Error) { + this.popupStack = this.popupStack.filter( + p => p.type !== PopupType.Error || p.error !== error + ) + } } diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 6e37e97476..77e2ea22f0 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -3939,6 +3939,7 @@ export class AppStore extends TypedBaseStore { const newErrors = Array.from(this.errors) newErrors.push(error) this.errors = newErrors + this.popupManager.addErrorPopup(error) this.emitUpdate() return Promise.resolve() @@ -3947,6 +3948,7 @@ export class AppStore extends TypedBaseStore { /** This shouldn't be called directly. See `Dispatcher`. */ public _clearError(error: Error): Promise { this.errors = this.errors.filter(e => e !== error) + this.popupManager.removeErrorPopup(error) this.emitUpdate() return Promise.resolve() From 22ca426f048a86773d9b866fc3b839e422ff405c Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 25 Oct 2022 10:45:35 +0200 Subject: [PATCH 139/262] Feature flag it! --- app/src/lib/feature-flag.ts | 5 +++++ app/src/main-process/app-window.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/app/src/lib/feature-flag.ts b/app/src/lib/feature-flag.ts index f411e68b1a..192c33d265 100644 --- a/app/src/lib/feature-flag.ts +++ b/app/src/lib/feature-flag.ts @@ -112,3 +112,8 @@ export function enableSubmoduleDiff(): boolean { export function enableStartingPullRequests(): boolean { return enableBetaFeatures() } + +/** Should we enable mechanism to prevent closing while the app is updating? */ +export function enablePreventClosingWhileUpdating(): boolean { + return enableBetaFeatures() +} diff --git a/app/src/main-process/app-window.ts b/app/src/main-process/app-window.ts index 7c1af9e7c9..740eccfa61 100644 --- a/app/src/main-process/app-window.ts +++ b/app/src/main-process/app-window.ts @@ -26,6 +26,7 @@ import { terminateDesktopNotifications, } from './notifications' import { addTrustedIPCSender } from './trusted-ipc-sender' +import { enablePreventClosingWhileUpdating } from '../lib/feature-flag' export class AppWindow { private window: Electron.BrowserWindow @@ -114,6 +115,7 @@ export class AppWindow { // app is updating, we will prevent the window from closing only when the // app is also quitting. if ( + enablePreventClosingWhileUpdating() && (!__DARWIN__ || quitting) && !quittingEvenIfUpdating && this.isDownloadingUpdate From 5374b3b8c7ed41665a26581770805f919838c97c Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Thu, 20 Oct 2022 18:06:03 +0200 Subject: [PATCH 140/262] Allow fetching at any time from menu --- app/src/main-process/menu/build-default-menu.ts | 6 ++++++ app/src/main-process/menu/menu-event.ts | 1 + app/src/ui/app.tsx | 12 ++++++++++++ 3 files changed, 19 insertions(+) diff --git a/app/src/main-process/menu/build-default-menu.ts b/app/src/main-process/menu/build-default-menu.ts index 5da9141700..fb4c22093d 100644 --- a/app/src/main-process/menu/build-default-menu.ts +++ b/app/src/main-process/menu/build-default-menu.ts @@ -285,6 +285,12 @@ export function buildDefaultMenu({ accelerator: 'CmdOrCtrl+Shift+P', click: emit('pull'), }, + { + id: 'fetch', + label: __DARWIN__ ? 'Fetch' : '&Fetch', + accelerator: 'CmdOrCtrl+Shift+T', + click: emit('fetch'), + }, { label: removeRepoLabel, id: 'remove-repository', diff --git a/app/src/main-process/menu/menu-event.ts b/app/src/main-process/menu/menu-event.ts index 235ff73e2b..a3eef2bdd5 100644 --- a/app/src/main-process/menu/menu-event.ts +++ b/app/src/main-process/menu/menu-event.ts @@ -2,6 +2,7 @@ export type MenuEvent = | 'push' | 'force-push' | 'pull' + | 'fetch' | 'show-changes' | 'show-history' | 'add-local-repository' diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 8a97914cbf..6b724f9e23 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -13,6 +13,7 @@ import { assertNever } from '../lib/fatal-error' import { shell } from '../lib/app-shell' import { updateStore, UpdateStatus } from './lib/update-store' import { RetryAction } from '../models/retry-actions' +import { FetchType } from '../models/fetch' import { shouldRenderApplicationMenu } from './lib/features' import { matchExistingRepository } from '../lib/repository-matching' import { getDotComAPIEndpoint } from '../lib/api' @@ -358,6 +359,8 @@ export class App extends React.Component { return this.push({ forceWithLease: true }) case 'pull': return this.pull() + case 'fetch': + return this.fetch() case 'show-changes': return this.showChanges() case 'show-history': @@ -949,6 +952,15 @@ export class App extends React.Component { this.props.dispatcher.pull(state.repository) } + private async fetch() { + const state = this.state.selectedState + if (state == null || state.type !== SelectionType.Repository) { + return + } + + this.props.dispatcher.fetch(state.repository, FetchType.UserInitiatedTask) + } + private showStashedChanges() { const state = this.state.selectedState if (state == null || state.type !== SelectionType.Repository) { From c4e60121e4e7e8aff371f990ff0ca27cdf554114 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Thu, 20 Oct 2022 18:08:01 +0200 Subject: [PATCH 141/262] Allow force-pushing from menu every time a branch has diverged --- app/src/lib/rebase.ts | 38 +++++++++++++++++++++++++++---- app/src/lib/stores/app-store.ts | 12 ++++++---- app/src/ui/app.tsx | 9 ++++++-- app/src/ui/changes/no-changes.tsx | 9 ++++++-- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/app/src/lib/rebase.ts b/app/src/lib/rebase.ts index 6ab9a6ddb1..2f8e68179f 100644 --- a/app/src/lib/rebase.ts +++ b/app/src/lib/rebase.ts @@ -3,6 +3,26 @@ import { IAheadBehind } from '../models/branch' import { TipState } from '../models/tip' import { clamp } from './clamp' +/** Represents the force-push availability state of a branch. */ +export enum ForcePushBranchState { + /** The branch cannot be force-pushed (it hasn't diverged from its upstream) */ + NotAvailable, + + /** + * The branch can be force-pushed, but the user didn't do any operation that + * we consider should be followed by a force-push, like rebasing or amending a + * pushed commit. + */ + Available, + + /** + * The branch can be force-pushed, and the user did some operation that we + * consider should be followed by a force-push, like rebasing or amending a + * pushed commit. + */ + Recommended, +} + /** * Format rebase percentage to ensure it's a value between 0 and 1, but to also * constrain it to two significant figures, avoiding the remainder that comes @@ -16,17 +36,23 @@ export function formatRebaseValue(value: number) { * Check application state to see whether the action applied to the current * branch should be a force push */ -export function isCurrentBranchForcePush( +export function getCurrentBranchForcePushState( branchesState: IBranchesState, aheadBehind: IAheadBehind | null -) { +): ForcePushBranchState { if (aheadBehind === null) { // no tracking branch found - return false + return ForcePushBranchState.NotAvailable + } + + const { ahead, behind } = aheadBehind + + if (behind === 0 || ahead === 0) { + // no a diverged branch to force push + return ForcePushBranchState.NotAvailable } const { tip, forcePushBranches } = branchesState - const { ahead, behind } = aheadBehind let canForcePushBranch = false if (tip.kind === TipState.Valid) { @@ -36,5 +62,7 @@ export function isCurrentBranchForcePush( canForcePushBranch = foundEntry === sha } - return canForcePushBranch && behind > 0 && ahead > 0 + return canForcePushBranch + ? ForcePushBranchState.Recommended + : ForcePushBranchState.Available } diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index afed3ee5c9..508f9bd0dd 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -180,7 +180,7 @@ import { matchExistingRepository, urlMatchesRemote, } from '../repository-matching' -import { isCurrentBranchForcePush } from '../rebase' +import { ForcePushBranchState, getCurrentBranchForcePushState } from '../rebase' import { RetryAction, RetryActionType } from '../../models/retry-actions' import { Default as DefaultShell, @@ -2231,10 +2231,12 @@ export class AppStore extends TypedBaseStore { ?.name ?? undefined } - const isForcePushForCurrentRepository = isCurrentBranchForcePush( - branchesState, - aheadBehind - ) + // From the menu, we'll offer to force-push whenever it's possible, regardless + // of whether or not the user performed any action we know whould be followed + // by a force-push. + const isForcePushForCurrentRepository = + getCurrentBranchForcePushState(branchesState, aheadBehind) !== + ForcePushBranchState.NotAvailable const isStashedChangesVisible = changesState.selection.kind === ChangesSelectionKind.Stash diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 8a97914cbf..c4591b2cef 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -92,7 +92,10 @@ import { RepositoryStateCache } from '../lib/stores/repository-state-cache' import { PopupType, Popup } from '../models/popup' import { OversizedFiles } from './changes/oversized-files-warning' import { PushNeedsPullWarning } from './push-needs-pull' -import { isCurrentBranchForcePush } from '../lib/rebase' +import { + ForcePushBranchState, + getCurrentBranchForcePushState, +} from '../lib/rebase' import { Banner, BannerType } from '../models/banner' import { StashAndSwitchBranch } from './stash-changes/stash-and-switch-branch-dialog' import { OverwriteStash } from './stash-changes/overwrite-stashed-changes-dialog' @@ -2735,7 +2738,9 @@ export class App extends React.Component { remoteName = tip.branch.upstreamRemoteName } - const isForcePush = isCurrentBranchForcePush(branchesState, aheadBehind) + const isForcePush = + getCurrentBranchForcePushState(branchesState, aheadBehind) === + ForcePushBranchState.Recommended return ( Date: Tue, 25 Oct 2022 13:19:30 +0200 Subject: [PATCH 142/262] Record Alive events received for recent repositories --- app/src/lib/stats/stats-database.ts | 12 ++++++++++++ app/src/lib/stats/stats-store.ts | 16 +++++++++++++++ app/src/lib/stores/app-store.ts | 3 +++ app/src/lib/stores/notifications-store.ts | 24 +++++++++++++++++++++++ 4 files changed, 55 insertions(+) diff --git a/app/src/lib/stats/stats-database.ts b/app/src/lib/stats/stats-database.ts index 7f597e1404..4fb554e0e9 100644 --- a/app/src/lib/stats/stats-database.ts +++ b/app/src/lib/stats/stats-database.ts @@ -467,6 +467,12 @@ export interface IDailyMeasures { /** The number of "checks failed" notifications the user received */ readonly checksFailedNotificationCount: number + /** + * The number of "checks failed" notifications the user received for a recent + * repository other than the selected one. + */ + readonly checksFailedNotificationFromRecentRepoCount: number + /** The number of "checks failed" notifications the user clicked */ readonly checksFailedNotificationClicked: number @@ -485,6 +491,12 @@ export interface IDailyMeasures { */ readonly checksFailedDialogRerunChecksCount: number + /** + * The number of PR review notifications the user received for a recent + * repository other than the selected one. + */ + readonly pullRequestReviewNotificationFromRecentRepoCount: number + /** The number of "approved PR" notifications the user received */ readonly pullRequestReviewApprovedNotificationCount: number diff --git a/app/src/lib/stats/stats-store.ts b/app/src/lib/stats/stats-store.ts index 1cb452a67c..7905848d56 100644 --- a/app/src/lib/stats/stats-store.ts +++ b/app/src/lib/stats/stats-store.ts @@ -195,10 +195,12 @@ const DefaultDailyMeasures: IDailyMeasures = { viewsCheckJobStepOnline: 0, rerunsChecks: 0, checksFailedNotificationCount: 0, + checksFailedNotificationFromRecentRepoCount: 0, checksFailedNotificationClicked: 0, checksFailedDialogOpenCount: 0, checksFailedDialogSwitchToPullRequestCount: 0, checksFailedDialogRerunChecksCount: 0, + pullRequestReviewNotificationFromRecentRepoCount: 0, pullRequestReviewApprovedNotificationCount: 0, pullRequestReviewApprovedNotificationClicked: 0, pullRequestReviewApprovedDialogSwitchToPullRequestCount: 0, @@ -1776,6 +1778,13 @@ export class StatsStore implements IStatsStore { })) } + public recordChecksFailedNotificationFromRecentRepo(): Promise { + return this.updateDailyMeasures(m => ({ + checksFailedNotificationFromRecentRepoCount: + m.checksFailedNotificationFromRecentRepoCount + 1, + })) + } + public recordChecksFailedNotificationClicked(): Promise { return this.updateDailyMeasures(m => ({ checksFailedNotificationClicked: m.checksFailedNotificationClicked + 1, @@ -1845,6 +1854,13 @@ export class StatsStore implements IStatsStore { return `pullRequestReview${infixMap[reviewType]}${suffix}` } + public recordPullRequestReviewNotiificationFromRecentRepo(): Promise { + return this.updateDailyMeasures(m => ({ + pullRequestReviewNotificationFromRecentRepoCount: + m.pullRequestReviewNotificationFromRecentRepoCount + 1, + })) + } + // Generic method to record stats related to Pull Request review notifications. private recordPullRequestReviewStat( reviewType: ValidNotificationPullRequestReviewState, diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index afed3ee5c9..8cdd2b76b7 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -1736,6 +1736,9 @@ export class AppStore extends TypedBaseStore { ) setNumberArray(RecentRepositoriesKey, slicedRecentRepositories) this.recentRepositories = slicedRecentRepositories + this.notificationsStore.setRecentRepositories( + this.repositories.filter(r => this.recentRepositories.includes(r.id)) + ) this.emitUpdate() } diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index 30f611b0a7..a1ad017f23 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -68,6 +68,7 @@ export function getNotificationsEnabled() { */ export class NotificationsStore { private repository: RepositoryWithGitHubRepository | null = null + private recentRepositories: ReadonlyArray = [] private onChecksFailedCallback: OnChecksFailedCallback | null = null private onPullRequestReviewSubmitCallback: OnPullRequestReviewSubmitCallback | null = null @@ -124,6 +125,9 @@ export class NotificationsStore { } if (!this.isValidRepositoryForEvent(repository, event)) { + if (this.isRecentRepositoryEvent(event)) { + this.statsStore.recordPullRequestReviewNotiificationFromRecentRepo() + } return } @@ -199,6 +203,9 @@ export class NotificationsStore { } if (!this.isValidRepositoryForEvent(repository, event)) { + if (this.isRecentRepositoryEvent(event)) { + this.statsStore.recordChecksFailedNotificationFromRecentRepo() + } return } @@ -318,6 +325,14 @@ export class NotificationsStore { ) } + private isRecentRepositoryEvent(event: DesktopAliveEvent) { + return this.recentRepositories.some( + r => + isRepositoryWithGitHubRepository(r) && + this.isValidRepositoryForEvent(r, event) + ) + } + /** * Makes the store to keep track of the currently selected repository. Only * notifications for the currently selected repository will be shown. @@ -328,6 +343,15 @@ export class NotificationsStore { : null } + /** + * 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) { + this.recentRepositories = repositories + } + private async getAccountForRepository(repository: GitHubRepository) { const { endpoint } = repository From 538e058dad6711f13f52788cc8e2fa1193c22462 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 25 Oct 2022 13:50:42 +0200 Subject: [PATCH 143/262] Also record notifications from non-recent repos? --- app/src/lib/stats/stats-database.ts | 12 ++++++++++++ app/src/lib/stats/stats-store.ts | 16 ++++++++++++++++ app/src/lib/stores/notifications-store.ts | 4 ++++ 3 files changed, 32 insertions(+) diff --git a/app/src/lib/stats/stats-database.ts b/app/src/lib/stats/stats-database.ts index 4fb554e0e9..49a02fa62d 100644 --- a/app/src/lib/stats/stats-database.ts +++ b/app/src/lib/stats/stats-database.ts @@ -473,6 +473,12 @@ export interface IDailyMeasures { */ readonly checksFailedNotificationFromRecentRepoCount: number + /** + * The number of "checks failed" notifications the user received for a + * non-recent repository other than the selected one. + */ + readonly checksFailedNotificationFromNonRecentRepoCount: number + /** The number of "checks failed" notifications the user clicked */ readonly checksFailedNotificationClicked: number @@ -497,6 +503,12 @@ export interface IDailyMeasures { */ readonly pullRequestReviewNotificationFromRecentRepoCount: number + /** + * The number of PR review notifications the user received for a non-recent + * repository other than the selected one. + */ + readonly pullRequestReviewNotificationFromNonRecentRepoCount: number + /** The number of "approved PR" notifications the user received */ readonly pullRequestReviewApprovedNotificationCount: number diff --git a/app/src/lib/stats/stats-store.ts b/app/src/lib/stats/stats-store.ts index 7905848d56..a3540bde0b 100644 --- a/app/src/lib/stats/stats-store.ts +++ b/app/src/lib/stats/stats-store.ts @@ -196,11 +196,13 @@ const DefaultDailyMeasures: IDailyMeasures = { rerunsChecks: 0, checksFailedNotificationCount: 0, checksFailedNotificationFromRecentRepoCount: 0, + checksFailedNotificationFromNonRecentRepoCount: 0, checksFailedNotificationClicked: 0, checksFailedDialogOpenCount: 0, checksFailedDialogSwitchToPullRequestCount: 0, checksFailedDialogRerunChecksCount: 0, pullRequestReviewNotificationFromRecentRepoCount: 0, + pullRequestReviewNotificationFromNonRecentRepoCount: 0, pullRequestReviewApprovedNotificationCount: 0, pullRequestReviewApprovedNotificationClicked: 0, pullRequestReviewApprovedDialogSwitchToPullRequestCount: 0, @@ -1785,6 +1787,13 @@ export class StatsStore implements IStatsStore { })) } + public recordChecksFailedNotificationFromNonRecentRepo(): Promise { + return this.updateDailyMeasures(m => ({ + checksFailedNotificationFromNonRecentRepoCount: + m.checksFailedNotificationFromNonRecentRepoCount + 1, + })) + } + public recordChecksFailedNotificationClicked(): Promise { return this.updateDailyMeasures(m => ({ checksFailedNotificationClicked: m.checksFailedNotificationClicked + 1, @@ -1861,6 +1870,13 @@ export class StatsStore implements IStatsStore { })) } + public recordPullRequestReviewNotiificationFromNonRecentRepo(): Promise { + return this.updateDailyMeasures(m => ({ + pullRequestReviewNotificationFromNonRecentRepoCount: + m.pullRequestReviewNotificationFromNonRecentRepoCount + 1, + })) + } + // Generic method to record stats related to Pull Request review notifications. private recordPullRequestReviewStat( reviewType: ValidNotificationPullRequestReviewState, diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index a1ad017f23..80c3c8b6b9 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -127,6 +127,8 @@ export class NotificationsStore { if (!this.isValidRepositoryForEvent(repository, event)) { if (this.isRecentRepositoryEvent(event)) { this.statsStore.recordPullRequestReviewNotiificationFromRecentRepo() + } else { + this.statsStore.recordPullRequestReviewNotiificationFromNonRecentRepo() } return } @@ -205,6 +207,8 @@ export class NotificationsStore { if (!this.isValidRepositoryForEvent(repository, event)) { if (this.isRecentRepositoryEvent(event)) { this.statsStore.recordChecksFailedNotificationFromRecentRepo() + } else { + this.statsStore.recordChecksFailedNotificationFromNonRecentRepo() } return } From 83daa84accf0730751017c3df3e922af53e5d3f9 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 25 Oct 2022 15:09:41 +0200 Subject: [PATCH 144/262] Update app/src/lib/stores/app-store.ts Co-authored-by: tidy-dev <75402236+tidy-dev@users.noreply.github.com> --- app/src/lib/stores/app-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 508f9bd0dd..9f05c51a59 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -2232,7 +2232,7 @@ export class AppStore extends TypedBaseStore { } // From the menu, we'll offer to force-push whenever it's possible, regardless - // of whether or not the user performed any action we know whould be followed + // of whether or not the user performed any action we know would be followed // by a force-push. const isForcePushForCurrentRepository = getCurrentBranchForcePushState(branchesState, aheadBehind) !== From f80fab55b4a3173efe70f07fe7678d7be41a67d8 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 25 Oct 2022 15:11:03 +0200 Subject: [PATCH 145/262] Update app/src/ui/installing-update/installing-update.tsx Co-authored-by: Markus Olsson --- app/src/ui/installing-update/installing-update.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index 239d04c85b..6aed3ab3ac 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -29,10 +29,6 @@ interface IInstallingUpdateProps { export class InstallingUpdate extends React.Component { private updateStoreEventHandle: Disposable | null = null - public constructor(props: IInstallingUpdateProps) { - super(props) - } - 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. From fe93c2957947b65fc835d4bbf37dd919cd2547d4 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 25 Oct 2022 15:20:19 +0200 Subject: [PATCH 146/262] Cancel quitting when the "Installing update" dialog is closed Co-Authored-By: Markus Olsson <634063+niik@users.noreply.github.com> --- app/src/ui/installing-update/installing-update.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/ui/installing-update/installing-update.tsx b/app/src/ui/installing-update/installing-update.tsx index 6aed3ab3ac..64531fc39d 100644 --- a/app/src/ui/installing-update/installing-update.tsx +++ b/app/src/ui/installing-update/installing-update.tsx @@ -51,17 +51,17 @@ export class InstallingUpdate extends React.Component { 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) } - private onCancel = () => { - this.props.dispatcher.cancelQuittingApp() - this.props.onDismissed() - } - public render() { return ( { title={__DARWIN__ ? 'Installing Update…' : 'Installing update…'} loading={true} dismissable={true} - onDismissed={this.onCancel} + onDismissed={this.props.onDismissed} /> @@ -86,7 +86,7 @@ export class InstallingUpdate extends React.Component { From 5d40a5159773bf6d1f6a0db96f0737ccb4d8a80e Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:33:23 -0400 Subject: [PATCH 147/262] Track errors as popups --- app/src/lib/app-state.ts | 2 +- app/src/lib/popup-manager.ts | 8 +-- app/src/lib/stores/app-store.ts | 26 ++++----- app/src/ui/app-error.tsx | 91 ++++++++++------------------- app/src/ui/app.tsx | 29 +++++---- app/src/ui/dispatcher/dispatcher.ts | 12 ++-- app/src/ui/index.tsx | 4 +- app/test/unit/popup-manager-test.ts | 71 +++++++++++++++++++++- 8 files changed, 144 insertions(+), 99 deletions(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index bd996cb09f..ac2ebb07ce 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -145,7 +145,7 @@ export interface IAppState { */ readonly appMenuState: ReadonlyArray - readonly errors: ReadonlyArray + readonly errorCount: number /** Map from the emoji shortcut (e.g., :+1:) to the image's local path. */ readonly emoji: Map diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index ed2450879c..ae3d42f49b 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -144,11 +144,9 @@ export class PopupManager { } /** - * Removes any popup with the given error + * Removes popup from the stack by it's id */ - public removeErrorPopup(error: Error) { - this.popupStack = this.popupStack.filter( - p => p.type !== PopupType.Error || p.error !== error - ) + public removePopupById(popupId: string) { + this.popupStack = this.popupStack.filter(p => p.id !== popupId) } } diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 77e2ea22f0..51cd8f2153 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -399,7 +399,6 @@ export class AppStore extends TypedBaseStore { private focusCommitMessage = false private currentFoldout: Foldout | null = null private currentBanner: Banner | null = null - private errors: ReadonlyArray = new Array() private emitQueued = false private readonly localRepositoryStateLookup = new Map< @@ -909,7 +908,7 @@ export class AppStore extends TypedBaseStore { signInState: this.signInStore.getState(), currentPopup: this.popupManager.currentPopup, currentFoldout: this.currentFoldout, - errors: this.errors, + errorCount: this.popupManager.getPopupsOfType(PopupType.Error).length, showWelcomeFlow: this.showWelcomeFlow, focusCommitMessage: this.focusCommitMessage, emoji: this.emoji, @@ -3518,6 +3517,17 @@ export class AppStore extends TypedBaseStore { this.emitUpdate() } + /** This shouldn't be called directly. See `Dispatcher`. */ + public _closePopupById(popupId: string) { + const currentPopup = this.popupManager.currentPopup + if (currentPopup === null) { + return + } + + this.popupManager.removePopupById(popupId) + this.emitUpdate() + } + /** This shouldn't be called directly. See `Dispatcher`. */ public async _showFoldout(foldout: Foldout): Promise { this.currentFoldout = foldout @@ -3936,24 +3946,12 @@ export class AppStore extends TypedBaseStore { /** This shouldn't be called directly. See `Dispatcher`. */ public _pushError(error: Error): Promise { - const newErrors = Array.from(this.errors) - newErrors.push(error) - this.errors = newErrors this.popupManager.addErrorPopup(error) this.emitUpdate() return Promise.resolve() } - /** This shouldn't be called directly. See `Dispatcher`. */ - public _clearError(error: Error): Promise { - this.errors = this.errors.filter(e => e !== error) - this.popupManager.removeErrorPopup(error) - this.emitUpdate() - - return Promise.resolve() - } - /** This shouldn't be called directly. See `Dispatcher`. */ public async _changeRepositoryAlias( repository: Repository, diff --git a/app/src/ui/app-error.tsx b/app/src/ui/app-error.tsx index b5b91d068f..4378808f05 100644 --- a/app/src/ui/app-error.tsx +++ b/app/src/ui/app-error.tsx @@ -9,7 +9,6 @@ import { import { dialogTransitionTimeout } from './app' import { GitError, isAuthFailureError } from '../lib/git/core' import { Popup, PopupType } from '../models/popup' -import { TransitionGroup, CSSTransition } from 'react-transition-group' import { OkCancelButtonGroup } from './dialog/ok-cancel-button-group' import { ErrorWithMetadata } from '../lib/error-with-metadata' import { RetryActionType, RetryAction } from '../models/retry-actions' @@ -18,14 +17,11 @@ import memoizeOne from 'memoize-one' import { parseCarriageReturn } from '../lib/parse-carriage-return' interface IAppErrorProps { - /** The list of queued, app-wide, errors */ - readonly errors: ReadonlyArray + /** The error to be displayed */ + readonly error: Error - /** - * A callback which is used whenever a particular error - * has been shown to, and been dismissed by, the user. - */ - readonly onClearError: (error: Error) => void + /** Called to dismiss the dialog */ + readonly onDismissed: () => void readonly onShowPopup: (popupType: Popup) => void | undefined readonly onRetryAction: (retryAction: RetryAction) => void } @@ -53,13 +49,13 @@ export class AppError extends React.Component { public constructor(props: IAppErrorProps) { super(props) this.state = { - error: props.errors[0] || null, + error: props.error, disabled: false, } } public componentWillReceiveProps(nextProps: IAppErrorProps) { - const error = nextProps.errors[0] || null + const error = nextProps.error // We keep the currently shown error until it has disappeared // from the first spot in the application error queue. @@ -69,18 +65,7 @@ export class AppError extends React.Component { } 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) - } + this.props.onDismissed() } private showPreferencesDialog = () => { @@ -128,36 +113,6 @@ export class AppError extends React.Component { return 'Error' } - private renderDialog() { - const error = this.state.error - - if (!error) { - return null - } - - return ( - - - {this.renderErrorMessage(error)} - {this.renderContentAfterErrorMessage(error)} - - {this.renderFooter(error)} - - ) - } - private renderContentAfterErrorMessage(error: Error) { if (!isErrorWithMetaData(error)) { return undefined @@ -257,16 +212,32 @@ export class AppError extends React.Component { } public render() { - const dialogContent = this.renderDialog() + const error = this.state.error + + if (!error) { + return null + } return ( - - {dialogContent && ( - - {dialogContent} - - )} - + + + {this.renderErrorMessage(error)} + {this.renderContentAfterErrorMessage(error)} + + {this.renderFooter(error)} + ) } } diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 286d2a5143..f7bb00615b 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -224,8 +224,8 @@ export class App extends React.Component { * passed popupType, so it can be used in render() without creating * multiple instances when the component gets re-rendered. */ - private getOnPopupDismissedFn = memoizeOne((popupType: PopupType) => { - return () => this.onPopupDismissed(popupType) + private getOnPopupDismissedFn = memoizeOne((popupId: string) => { + return () => this.onPopupDismissed(popupId) }) public constructor(props: IAppProps) { @@ -348,7 +348,7 @@ export class App extends React.Component { private onMenuEvent(name: MenuEvent): any { // Don't react to menu events when an error dialog is shown. - if (name !== 'show-app-error' && this.state.errors.length) { + if (name !== 'show-app-error' && this.state.errorCount > 1) { return } @@ -1358,8 +1358,8 @@ export class App extends React.Component { ) } - private onPopupDismissed = (popupType: PopupType) => { - return this.props.dispatcher.closePopup(popupType) + private onPopupDismissed = (popupId: string) => { + return this.props.dispatcher.closePopupById(popupId) } private onContinueWithUntrustedCertificate = ( @@ -1381,7 +1381,18 @@ export class App extends React.Component { return null } - const onPopupDismissedFn = this.getOnPopupDismissedFn(popup.type) + if (popup.id === undefined) { + // Should not be possible... but if it does we want to know about it. + sendNonFatalException( + 'PopupNoId', + new Error( + `Attempted to open a popup of type '${popup.type}' without an Id` + ) + ) + return null + } + + const onPopupDismissedFn = this.getOnPopupDismissedFn(popup.id) switch (popup.type) { case PopupType.RenameBranch: @@ -2301,8 +2312,8 @@ export class App extends React.Component { case PopupType.Error: { return ( @@ -2473,8 +2484,6 @@ export class App extends React.Component { return } - private clearError = (error: Error) => this.props.dispatcher.clearError(error) - private onConfirmDiscardChangesChanged = (value: boolean) => { this.props.dispatcher.setConfirmDiscardChangesSetting(value) } diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index 897940c089..365d4b4733 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -384,6 +384,13 @@ export class Dispatcher { return this.appStore._closePopup(popupType) } + /** + * Close the popup with given id. + */ + public closePopupById(popupId: string) { + return this.appStore._closePopupById(popupId) + } + /** Show the foldout. This will close any current popup. */ public showFoldout(foldout: Foldout): Promise { return this.appStore._showFoldout(foldout) @@ -765,11 +772,6 @@ export class Dispatcher { return this.appStore._pushError(error) } - /** Clear the given error. */ - public clearError(error: Error): Promise { - return this.appStore._clearError(error) - } - /** * Clone a missing repository to the previous path, and update it's * state in the repository list if the clone completes without error. diff --git a/app/src/ui/index.tsx b/app/src/ui/index.tsx index 2d883d5606..caa09e662f 100644 --- a/app/src/ui/index.tsx +++ b/app/src/ui/index.tsx @@ -168,8 +168,8 @@ const sendErrorWithContext = ( extra.windowZoomFactor = `${currentState.windowZoomFactor}` } - if (currentState.errors.length > 0) { - extra.activeAppErrors = `${currentState.errors.length}` + if (currentState.errorCount > 0) { + extra.activeAppErrors = `${currentState.errorCount}` } extra.repositoryCount = `${currentState.repositories.length}` diff --git a/app/test/unit/popup-manager-test.ts b/app/test/unit/popup-manager-test.ts index 4d89919ffd..85eee5759f 100644 --- a/app/test/unit/popup-manager-test.ts +++ b/app/test/unit/popup-manager-test.ts @@ -21,7 +21,7 @@ describe('PopupManager', () => { expect(currentPopup).toBeNull() }) - it('returns last added popup', () => { + it('returns last added non-error popup', () => { const popupAbout: Popup = { type: PopupType.About } const popupSignIn: Popup = { type: PopupType.SignIn } popupManager.addPopup(popupAbout) @@ -31,6 +31,18 @@ describe('PopupManager', () => { 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', () => { @@ -155,6 +167,39 @@ describe('PopupManager', () => { }) }) + 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, '') @@ -233,7 +278,7 @@ describe('PopupManager', () => { }) describe('removePopupByType', () => { - it('returns popups of a given type', () => { + it('removes the popups of a given type', () => { popupManager.addPopup({ type: PopupType.About }) popupManager.addPopup({ type: PopupType.SignIn, @@ -248,4 +293,26 @@ describe('PopupManager', () => { 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) + }) + }) }) From 7c94afab6728e8fd52673fbf7cd9b744668beac0 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:46:41 -0400 Subject: [PATCH 148/262] Handle error adding through addPopup --- app/src/lib/popup-manager.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index ae3d42f49b..f8986b31c6 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -56,10 +56,14 @@ export class PopupManager { * - 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) if (!enableStackedPopups()) { - this.popupStack = [popupToAdd] + this.popupStack = [popupToAdd, ...this.getPopupsOfType(PopupType.Error)] return popupToAdd } From ce034d64d102eeefee9c8e334803b9642b7a0c2f Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 25 Oct 2022 11:05:14 -0400 Subject: [PATCH 149/262] Notes... --- app/src/lib/popup-manager.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index f8986b31c6..91f6a3e8e6 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -12,6 +12,29 @@ 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 = new Array() From e655a5809c3cabcbdff5eb39a8ddfe468f7538a9 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 25 Oct 2022 11:52:20 -0400 Subject: [PATCH 150/262] Update app-store.ts --- app/src/lib/stores/app-store.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 51cd8f2153..6ffb9dd4e9 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -3519,8 +3519,7 @@ export class AppStore extends TypedBaseStore { /** This shouldn't be called directly. See `Dispatcher`. */ public _closePopupById(popupId: string) { - const currentPopup = this.popupManager.currentPopup - if (currentPopup === null) { + if (this.popupManager.currentPopup === null) { return } From 254e26dfb1be16e91b0e3732e4f0e7b8ddabc450 Mon Sep 17 00:00:00 2001 From: Angus Date: Wed, 26 Oct 2022 00:45:18 +0100 Subject: [PATCH 151/262] Hide window instead of hide the app in Mac --- app/src/main-process/app-window.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main-process/app-window.ts b/app/src/main-process/app-window.ts index 073509acac..693fb73351 100644 --- a/app/src/main-process/app-window.ts +++ b/app/src/main-process/app-window.ts @@ -104,9 +104,9 @@ export class AppWindow { // https://github.com/desktop/desktop/issues/12838 if (this.window.isFullScreen()) { this.window.setFullScreen(false) - this.window.once('leave-full-screen', () => app.hide()) + this.window.once('leave-full-screen', () => this.window.hide()) } else { - app.hide() + this.window.hide() } return } From 008a0fb793b46b18ff3ed157830a8076d95563e2 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 26 Oct 2022 09:50:25 -0400 Subject: [PATCH 152/262] Release 3.1.3-beta2 --- app/package.json | 2 +- changelog.json | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index ec02226408..e22e0975dc 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "productName": "GitHub Desktop", "bundleID": "com.github.GitHubClient", "companyName": "GitHub, Inc.", - "version": "3.1.3-beta1", + "version": "3.1.3-beta2", "main": "./main.js", "repository": { "type": "git", diff --git a/changelog.json b/changelog.json index 5099e3dc00..973ee50375 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,20 @@ { "releases": { + "3.1.3-beta2": [ + "[Added] Enable menu option to Force-push branches that have diverged - #15521", + "[Added] Add menu option to Fetch the current repository at any time - #15520", + "[Added] Support CLion as an external editor - #13739. Thanks @daniel-ciaglia!", + "[Fixed] Prevent closing the GitHub Desktop while it's being updated - #15416", + "[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 - #15475. Thanks @angusdev!", + "[Fixed] Close 'Resolve conflicts before Rebase' dialog will not disable menu items - #15471. Thanks @angusdev!", + "[Fixed] Tooltips are positioned properly if mouse is not moved - #15462. Thanks @angusdev!", + "[Fixed] Fix tooltips of long commit author emails not breaking properly - #15460. Thanks @angusdev!", + "[Fixed] Clone repository progress bar no longer hidden by repository list - #15465. Thanks @angusdev!", + "[Fixed] Fix commit shortcut (Ctrl/Cmd + Enter) while amending a commit - #15445", + "[Improved] Pull request preview dialog width and height is responsive - #15500" + ], "3.1.3-beta1": ["[Improved] Upgrade embedded Git to 2.35.5"], "3.1.2": ["[Improved] Upgrade embedded Git to 2.35.5"], "3.1.2-beta1": [ From ab44201bc819c9f58a2e5e59a7b4eb85efc861f6 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 26 Oct 2022 09:56:28 -0400 Subject: [PATCH 153/262] Update changelog.json --- changelog.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.json b/changelog.json index 973ee50375..4751218377 100644 --- a/changelog.json +++ b/changelog.json @@ -3,7 +3,7 @@ "3.1.3-beta2": [ "[Added] Enable menu option to Force-push branches that have diverged - #15521", "[Added] Add menu option to Fetch the current repository at any time - #15520", - "[Added] Support CLion as an external editor - #13739. Thanks @daniel-ciaglia!", + "[Added] Support VSCodium as an external editor - #15348. Thanks @daniel-ciaglia!", "[Fixed] Prevent closing the GitHub Desktop while it's being updated - #15416", "[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", From 8d80d129d695fb06f00072671337dc6600dfc41f Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 26 Oct 2022 14:05:23 -0400 Subject: [PATCH 154/262] Add default update error message --- app/src/ui/app.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 61161fe887..5ef2e80663 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -284,7 +284,14 @@ export class App extends React.Component { updateStore.onError(error => { log.error(`Error checking for updates`, error) - this.props.dispatcher.postError(error) + // It is possible to obtain an error with no message. This was found to be + // the case on a windows instance where there was not space on the hard + // drive to download the installer. In this case, we want to override the + // error message so the user is not given a blank dialog. + const isErrorMsg = error.message.trim().length > 0 + this.props.dispatcher.postError( + isErrorMsg ? error : new Error('Checking for updates failed.') + ) }) ipcRenderer.on('launch-timing-stats', (_, stats) => { From 242502275297bd83c13fee95dfccd42535c1c002 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 26 Oct 2022 14:11:14 -0400 Subject: [PATCH 155/262] Update app.tsx --- app/src/ui/app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 5ef2e80663..10e40ac0e9 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -288,9 +288,9 @@ export class App extends React.Component { // 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 isErrorMsg = error.message.trim().length > 0 + const hasErrorMsg = error.message.trim().length > 0 this.props.dispatcher.postError( - isErrorMsg ? error : new Error('Checking for updates failed.') + hasErrorMsg ? error : new Error('Checking for updates failed.') ) }) From 0cce7896fc174b3da5015a16cd03a4d404912161 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Thu, 27 Oct 2022 13:33:24 +0200 Subject: [PATCH 156/262] Skip cache retrieving checks for a notification --- app/src/lib/api.ts | 15 +++++++++++---- app/src/lib/stores/notifications-store.ts | 6 ++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/lib/api.ts b/app/src/lib/api.ts index a965ae1a93..bc17c4bcd8 100644 --- a/app/src/lib/api.ts +++ b/app/src/lib/api.ts @@ -1053,11 +1053,14 @@ export class API { public async fetchCombinedRefStatus( owner: string, name: string, - ref: string + ref: string, + reloadCache: boolean = false ): Promise { const safeRef = encodeURIComponent(ref) const path = `repos/${owner}/${name}/commits/${safeRef}/status?per_page=100` - const response = await this.request('GET', path) + const response = await this.request('GET', path, { + reloadCache, + }) try { return await parsedResponse(response) @@ -1076,7 +1079,8 @@ export class API { public async fetchRefCheckRuns( owner: string, name: string, - ref: string + ref: string, + reloadCache: boolean = false ): Promise { const safeRef = encodeURIComponent(ref) const path = `repos/${owner}/${name}/commits/${safeRef}/check-runs?per_page=100` @@ -1084,7 +1088,10 @@ export class API { Accept: 'application/vnd.github.antiope-preview+json', } - const response = await this.request('GET', path, { customHeaders: headers }) + const response = await this.request('GET', path, { + customHeaders: headers, + reloadCache, + }) try { return await parsedResponse(response) diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index 30f611b0a7..6510e61486 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -358,9 +358,11 @@ export class NotificationsStore { return null } + // Hit these API endpoints reloading the cache to make sure we have the + // latest data at the time the notification is received. const [statuses, checkRuns] = await Promise.all([ - api.fetchCombinedRefStatus(owner.login, name, ref), - api.fetchRefCheckRuns(owner.login, name, ref), + api.fetchCombinedRefStatus(owner.login, name, ref, true), + api.fetchRefCheckRuns(owner.login, name, ref, true), ]) const checks = new Array() From 3d353a34fbdb479d257d319445019bfa8e8e605b Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Thu, 27 Oct 2022 13:34:03 +0200 Subject: [PATCH 157/262] Use the right repository and ref when looking for checks --- app/src/lib/stores/notifications-store.ts | 27 +++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index 6510e61486..634ee34489 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -5,7 +5,7 @@ import { isRepositoryWithForkedGitHubRepository, } from '../../models/repository' import { ForkContributionTarget } from '../../models/workflow-preferences' -import { PullRequest } from '../../models/pull-request' +import { getPullRequestCommitRef, PullRequest } from '../../models/pull-request' import { API, APICheckConclusion } from '../api' import { createCombinedCheckFromChecks, @@ -244,7 +244,20 @@ export class NotificationsStore { return } - const checks = await this.getChecksForRef(repository, pullRequest.head.ref) + const isForkContributingToParent = + isRepositoryWithForkedGitHubRepository(repository) && + repository.workflowPreferences.forkContributionTarget === + ForkContributionTarget.Parent + + // Checks must be retrieved from the repository the PR belongs to + const checksRepository = isForkContributingToParent + ? repository.gitHubRepository.parent + : repository.gitHubRepository + + const checks = await this.getChecksForRef( + checksRepository, + getPullRequestCommitRef(pullRequest.pullRequestNumber) + ) if (checks === null) { return } @@ -345,14 +358,10 @@ export class NotificationsStore { return API.fromAccount(account) } - private async getChecksForRef( - repository: RepositoryWithGitHubRepository, - ref: string - ) { - const { gitHubRepository } = repository - const { owner, name } = gitHubRepository + private async getChecksForRef(repository: GitHubRepository, ref: string) { + const { owner, name } = repository - const api = await this.getAPIForRepository(gitHubRepository) + const api = await this.getAPIForRepository(repository) if (api === null) { return null From 875184fe4e3fdc8e7547c61b3648eedbe5fd7fec Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Thu, 27 Oct 2022 13:34:57 +0200 Subject: [PATCH 158/262] Ignore notifications from known suites --- app/src/lib/stores/notifications-store.ts | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index 634ee34489..07ca42a1b1 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -73,6 +73,7 @@ export class NotificationsStore { null private cachedCommits: Map = new Map() private skipCommitShas: Set = new Set() + private skipCheckSuites: Set = new Set() public constructor( private readonly accountsStore: AccountsStore, @@ -202,6 +203,10 @@ export class NotificationsStore { return } + if (this.skipCheckSuites.has(event.check_suite_id)) { + return + } + const pullRequests = await this.pullRequestCoordinator.getAllPullRequests( repository ) @@ -273,6 +278,14 @@ export class NotificationsStore { return } + // Ignore any remaining notification for check suites that started along + // with this one. + for (const check of checks) { + if (check.checkSuiteId !== null) { + this.skipCheckSuites.add(check.checkSuiteId) + } + } + const pluralChecks = numberOfFailedChecks === 1 ? 'check was' : 'checks were' @@ -336,9 +349,20 @@ export class NotificationsStore { * notifications for the currently selected repository will be shown. */ public selectRepository(repository: Repository) { + if (repository.hash === this.repository?.hash) { + return + } + this.repository = isRepositoryWithGitHubRepository(repository) ? repository : null + this.resetCache() + } + + private resetCache() { + this.cachedCommits.clear() + this.skipCommitShas.clear() + this.skipCheckSuites.clear() } private async getAccountForRepository(repository: GitHubRepository) { From 5a3bad26ec79f9eed779cf8ee00e03a5aebfcf16 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 27 Oct 2022 08:21:07 -0400 Subject: [PATCH 159/262] Refer to issues if possible --- changelog.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/changelog.json b/changelog.json index 4751218377..2562d8c5ba 100644 --- a/changelog.json +++ b/changelog.json @@ -1,17 +1,17 @@ { "releases": { "3.1.3-beta2": [ - "[Added] Enable menu option to Force-push branches that have diverged - #15521", - "[Added] Add menu option to Fetch the current repository at any time - #15520", + "[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 - #15416", + "[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 - #15475. Thanks @angusdev!", - "[Fixed] Close 'Resolve conflicts before Rebase' dialog will not disable menu items - #15471. Thanks @angusdev!", - "[Fixed] Tooltips are positioned properly if mouse is not moved - #15462. Thanks @angusdev!", - "[Fixed] Fix tooltips of long commit author emails not breaking properly - #15460. Thanks @angusdev!", - "[Fixed] Clone repository progress bar no longer hidden by repository list - #15465. Thanks @angusdev!", + "[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" ], From 6548088a045b8ad261435838896de7b99b89a97a Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Fri, 28 Oct 2022 12:07:12 +0200 Subject: [PATCH 160/262] Update app/styles/ui/_side-by-side-diff.scss Co-authored-by: tidy-dev <75402236+tidy-dev@users.noreply.github.com> --- app/styles/ui/_side-by-side-diff.scss | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/app/styles/ui/_side-by-side-diff.scss b/app/styles/ui/_side-by-side-diff.scss index c512cb2045..cc99fe2c89 100644 --- a/app/styles/ui/_side-by-side-diff.scss +++ b/app/styles/ui/_side-by-side-diff.scss @@ -336,16 +336,12 @@ 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); } - &:not(.selectable) span { - // This must be the height of an octicon, so that when a hunk handle - // of any type is replaced by an "empty" hunk handle (e.g. when a - // file is completely expanded), the height of the whole line - // remains the same. - height: 16px; - } } } From 28182464643dbedeeb0ab49b1c28738638319c3b Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Fri, 28 Oct 2022 17:29:09 +0200 Subject: [PATCH 161/262] Fix linting again --- app/styles/ui/_side-by-side-diff.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/ui/_side-by-side-diff.scss b/app/styles/ui/_side-by-side-diff.scss index cc99fe2c89..5e71cbe9e6 100644 --- a/app/styles/ui/_side-by-side-diff.scss +++ b/app/styles/ui/_side-by-side-diff.scss @@ -338,7 +338,7 @@ border-color: var(--diff-hunk-gutter-background-color); align-self: stretch; align-items: center; - + &.selectable:hover { border-color: var(--diff-hover-background-color); } From 53c78bc53c8105ff6a0378f9784144a4f2be9d63 Mon Sep 17 00:00:00 2001 From: Angus Date: Tue, 1 Nov 2022 02:01:27 +0000 Subject: [PATCH 162/262] Close the repository list after repository is successfully created --- app/src/ui/add-repository/create-repository.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/add-repository/create-repository.tsx b/app/src/ui/add-repository/create-repository.tsx index d7a3846db8..96165be107 100644 --- a/app/src/ui/add-repository/create-repository.tsx +++ b/app/src/ui/add-repository/create-repository.tsx @@ -272,7 +272,6 @@ export class CreateRepository extends React.Component< this.setState({ creating: true }) - this.props.dispatcher.closeFoldout(FoldoutType.Repository) try { await initGitRepository(fullPath) } catch (e) { @@ -393,6 +392,7 @@ export class CreateRepository extends React.Component< this.updateDefaultDirectory() + this.props.dispatcher.closeFoldout(FoldoutType.Repository) this.props.dispatcher.selectRepository(repository) this.props.dispatcher.recordCreateRepository() this.props.onDismissed() From 00f8b5f60e21509895de901db48bd301c41397e5 Mon Sep 17 00:00:00 2001 From: Angus Date: Tue, 1 Nov 2022 10:01:50 +0800 Subject: [PATCH 163/262] Refactoring Co-authored-by: Sergio Padrino --- app/src/ui/add-repository/add-existing-repository.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/add-repository/add-existing-repository.tsx b/app/src/ui/add-repository/add-existing-repository.tsx index bc0cba7efc..3ea7882230 100644 --- a/app/src/ui/add-repository/add-existing-repository.tsx +++ b/app/src/ui/add-repository/add-existing-repository.tsx @@ -266,7 +266,7 @@ export class AddExistingRepository extends React.Component< const repositories = await dispatcher.addRepositories([resolvedPath]) if (repositories.length > 0) { - this.props.dispatcher.closeFoldout(FoldoutType.Repository) + dispatcher.closeFoldout(FoldoutType.Repository) dispatcher.selectRepository(repositories[0]) dispatcher.recordAddExistingRepository() } From ce49a37beea6c2acd4103554619a198f4f11d9d0 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Tue, 1 Nov 2022 10:06:09 -0400 Subject: [PATCH 164/262] Fix #15536 --- app/src/lib/stores/app-store.ts | 23 ++++++++++++++++++++++- app/src/ui/history/commit-list.tsx | 20 ++++++++------------ app/src/ui/lib/list/list.tsx | 8 ++++++++ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 5e9951667d..4348f616fd 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -6590,6 +6590,25 @@ export class AppStore extends TypedBaseStore { } } + /** + * Multi selection on the commit list can give an order of 1, 5, 3 if that is + * how the user selected them. However, we want to main chronological ordering + * of the commits to reduce the chance of conflicts during interact rebasing. + * Thus, assuming 1 is the first commit made by the user and 5 is the last. We + * want the order to be, 1, 3, 5. + */ + private orderCommitsByHistory( + repository: Repository, + commits: ReadonlyArray + ) { + const { compareState } = this.repositoryStateCache.get(repository) + const { commitSHAs } = compareState + + return [...commits].sort( + (a, b) => commitSHAs.indexOf(b.sha) - commitSHAs.indexOf(a.sha) + ) + } + /** This shouldn't be called directly. See `Dispatcher`. */ public async _cherryPick( repository: Repository, @@ -6600,13 +6619,15 @@ export class AppStore extends TypedBaseStore { return CherryPickResult.UnableToStart } + const orderedCommits = this.orderCommitsByHistory(repository, commits) + await this._refreshRepository(repository) const progressCallback = this.getMultiCommitOperationProgressCallBack(repository) const gitStore = this.gitStoreCache.get(repository) const result = await gitStore.performFailableOperation(() => - cherryPick(repository, commits, progressCallback) + cherryPick(repository, orderedCommits, progressCallback) ) return result || CherryPickResult.Error diff --git a/app/src/ui/history/commit-list.tsx b/app/src/ui/history/commit-list.tsx index 9b4734b269..912bdd5f99 100644 --- a/app/src/ui/history/commit-list.tsx +++ b/app/src/ui/history/commit-list.tsx @@ -275,15 +275,9 @@ export class CommitList extends React.Component { } private onSelectionChanged = (rows: ReadonlyArray) => { - // Multi select can give something like 1, 5, 3 depending on order that user - // selects. We want to ensure they are in chronological order for best - // cherry-picking results. If user wants to use cherry-picking for - // reordering, they will need to do multiple cherry-picks. - // Goal: first commit in history -> first on array - const sorted = [...rows].sort((a, b) => b - a) - const selectedShas = sorted.map(r => this.props.commitSHAs[r]) + const selectedShas = rows.map(r => this.props.commitSHAs[r]) const selectedCommits = this.lookupCommits(selectedShas) - this.props.onCommitsSelected?.(selectedCommits, this.isContiguous(sorted)) + this.props.onCommitsSelected?.(selectedCommits, this.isContiguous(rows)) } /** @@ -297,13 +291,15 @@ export class CommitList extends React.Component { return true } - for (let i = 0; i < indexes.length; i++) { - const current = indexes[i] - if (i + 1 === indexes.length) { + const sorted = [...indexes].sort((a, b) => b - a) + + for (let i = 0; i < sorted.length; i++) { + const current = sorted[i] + if (i + 1 === sorted.length) { continue } - if (current - 1 !== indexes[i + 1]) { + if (current - 1 !== sorted[i + 1]) { return false } } diff --git a/app/src/ui/lib/list/list.tsx b/app/src/ui/lib/list/list.tsx index 6f496c3573..4ff4c06fa9 100644 --- a/app/src/ui/lib/list/list.tsx +++ b/app/src/ui/lib/list/list.tsx @@ -74,6 +74,14 @@ interface IListProps { * The currently selected rows indexes. Used to attach a special * selection class on those row's containers as well as being used * for keyboard selection. + * + * It is expected that the use case for this is setting of the initially + * selected rows or clearing a list selection. + * + * N.B. Since it is used for keyboard selection, changing the ordering of + * elements in this array in a parent component may result in unexpected + * behaviors when a user modifies their selection via key commands. + * See #15536 lessons learned. */ readonly selectedRows: ReadonlyArray From c66d7c14c1a39c023b01529bf67b42eb653020ba Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Wed, 2 Nov 2022 19:24:30 -0400 Subject: [PATCH 165/262] Add .ino as a C++ source file --- app/src/highlighter/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/highlighter/index.ts b/app/src/highlighter/index.ts index 63885be640..5643535db4 100644 --- a/app/src/highlighter/index.ts +++ b/app/src/highlighter/index.ts @@ -148,6 +148,7 @@ const extensionModes: ReadonlyArray = [ '.h': 'text/x-c', '.cpp': 'text/x-c++src', '.hpp': 'text/x-c++src', + '.ino': 'text/x-c++src', '.kt': 'text/x-kotlin', }, }, From bbfaa6ea5b8417a6a450892ff8e5d506a52a72fa Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Fri, 4 Nov 2022 10:38:27 -0400 Subject: [PATCH 166/262] Always have an id --- app/src/lib/popup-manager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 91f6a3e8e6..ad74aabab7 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -85,9 +85,10 @@ export class PopupManager { const existingPopup = this.getPopupsOfType(popupToAdd.type) + const popup = { id: uuid(), ...popupToAdd } if (!enableStackedPopups()) { - this.popupStack = [popupToAdd, ...this.getPopupsOfType(PopupType.Error)] - return popupToAdd + this.popupStack = [popup, ...this.getPopupsOfType(PopupType.Error)] + return popup } if (existingPopup.length > 0) { @@ -97,7 +98,6 @@ export class PopupManager { return popupToAdd } - const popup = { id: uuid(), ...popupToAdd } this.popupStack.push(popup) this.checkStackLength() return popup From e9d1194e98ee207e2bafc8232507e6d38c5b0b5a Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Fri, 4 Nov 2022 10:44:01 -0400 Subject: [PATCH 167/262] Remove redundant onDismissed --- app/src/ui/app-error.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/app/src/ui/app-error.tsx b/app/src/ui/app-error.tsx index 4378808f05..427b78f3c0 100644 --- a/app/src/ui/app-error.tsx +++ b/app/src/ui/app-error.tsx @@ -64,12 +64,8 @@ export class AppError extends React.Component { } } - private onDismissed = () => { - this.props.onDismissed() - } - private showPreferencesDialog = () => { - this.onDismissed() + this.props.onDismissed() //This is a hacky solution to resolve multiple dialog windows //being open at the same time. @@ -80,7 +76,7 @@ export class AppError extends React.Component { private onRetryAction = (event: React.MouseEvent) => { event.preventDefault() - this.onDismissed() + this.props.onDismissed() const { error } = this.state @@ -162,7 +158,7 @@ export class AppError extends React.Component { private onCloseButtonClick = (e: React.MouseEvent) => { e.preventDefault() - this.onDismissed() + this.props.onDismissed() } private renderFooter(error: Error) { @@ -225,8 +221,8 @@ export class AppError extends React.Component { key="error" title={this.getTitle(error)} dismissable={false} - onSubmit={this.onDismissed} - onDismissed={this.onDismissed} + onSubmit={this.props.onDismissed} + onDismissed={this.props.onDismissed} disabled={this.state.disabled} className={ isRawGitError(this.state.error) ? 'raw-git-error' : undefined From adbe0fd43fb66c29262a7d79f18d94742d30f633 Mon Sep 17 00:00:00 2001 From: Shivareddy-Aluri Date: Mon, 7 Nov 2022 23:34:25 +0530 Subject: [PATCH 168/262] review changes --- app/src/ui/history/commit-list-item.tsx | 7 ++++--- app/src/ui/history/commit-summary.tsx | 4 ++-- app/styles/ui/history/_commit-summary.scss | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/ui/history/commit-list-item.tsx b/app/src/ui/history/commit-list-item.tsx index c6acedad30..fba4744e59 100644 --- a/app/src/ui/history/commit-list-item.tsx +++ b/app/src/ui/history/commit-list-item.tsx @@ -220,7 +220,7 @@ export class CommitListItem extends React.PureComponent< } private onCopyTags = () => { - clipboard.writeText(this.props.commit.tags.join(', ')) + clipboard.writeText(this.props.commit.tags.join(' ')) } private onViewOnGitHub = () => { @@ -345,7 +345,8 @@ export class CommitListItem extends React.PureComponent< deleteTagsMenuItem ) } - + const darwinTagsLabel = this.props.commit.tags.length > 1 ? 'Copy Tags' : 'Copy Tag'; + const windowTagsLabel = this.props.commit.tags.length > 1 ? 'Copy tags' : 'Copy tag'; items.push( { label: __DARWIN__ ? 'Cherry-pick Commit…' : 'Cherry-pick commit…', @@ -358,7 +359,7 @@ export class CommitListItem extends React.PureComponent< action: this.onCopySHA, }, { - label: 'Copy Tags', + label: __DARWIN__ ? darwinTagsLabel : windowTagsLabel, action: this.onCopyTags, enabled: this.props.commit.tags.length > 0 }, diff --git a/app/src/ui/history/commit-summary.tsx b/app/src/ui/history/commit-summary.tsx index af82a4156e..6437d4b0c9 100644 --- a/app/src/ui/history/commit-summary.tsx +++ b/app/src/ui/history/commit-summary.tsx @@ -430,7 +430,7 @@ export class CommitSummary extends React.Component< aria-label="SHA" > - + ) } @@ -641,7 +641,7 @@ export class CommitSummary extends React.Component< - {tags.join(', ')} + {tags.join(', ')} ) } diff --git a/app/styles/ui/history/_commit-summary.scss b/app/styles/ui/history/_commit-summary.scss index 0a025c2af2..9e7990d3c6 100644 --- a/app/styles/ui/history/_commit-summary.scss +++ b/app/styles/ui/history/_commit-summary.scss @@ -84,6 +84,7 @@ // for a class name one finds in tsx. We might want to consider not allowing // this in the future but that's for... the future. .commit-summary { + &-title, &-meta { padding: var(--spacing); @@ -175,7 +176,7 @@ vertical-align: bottom; // For some reason, `bottom` places the text in the middle } - .sha { + .selectable { user-select: text; } From 33f735f198d2bdbf3e8a3804e4bf85e5b4bd30a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Nov 2022 00:44:18 +0000 Subject: [PATCH 169/262] Bump loader-utils from 1.1.0 to 1.4.1 Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.1.0 to 1.4.1. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v1.1.0...v1.4.1) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8ee974004b..4c8ec8babf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2445,11 +2445,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -3677,11 +3672,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" @@ -6459,11 +6449,6 @@ json5@2.x, json5@^2.1.2: dependencies: minimist "^1.2.5" -json5@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -6691,13 +6676,13 @@ loader-runner@^4.2.0: integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== loader-utils@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" - integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= + version "1.4.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.1.tgz#278ad7006660bccc4d2c0c1578e17c5c78d5c0e0" + integrity sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q== dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" loader-utils@^2.0.0: version "2.0.0" @@ -7112,11 +7097,16 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.1.1, minimist@^1.2.0: +minimist@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@^1.2.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + 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" From 049ef06ad31c1e990cc2f199ceeda16f726c633e Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 8 Nov 2022 11:17:20 +0100 Subject: [PATCH 170/262] Use the right repo to fetch PR reviews --- app/src/lib/stores/notifications-store.ts | 31 ++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index f99128b716..64963f68ea 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -147,16 +147,17 @@ export class NotificationsStore { return } - const { gitHubRepository } = repository - const api = await this.getAPIForRepository(gitHubRepository) + // PR reviews must be retrieved from the repository the PR belongs to + const pullsRepository = this.getContributingRepository(repository) + const api = await this.getAPIForRepository(pullsRepository) if (api === null) { return } const review = await api.fetchPullRequestReview( - gitHubRepository.owner.login, - gitHubRepository.name, + pullsRepository.owner.login, + pullsRepository.name, pullRequest.pullRequestNumber.toString(), event.review_id ) @@ -260,15 +261,8 @@ export class NotificationsStore { return } - const isForkContributingToParent = - isRepositoryWithForkedGitHubRepository(repository) && - repository.workflowPreferences.forkContributionTarget === - ForkContributionTarget.Parent - // Checks must be retrieved from the repository the PR belongs to - const checksRepository = isForkContributingToParent - ? repository.gitHubRepository.parent - : repository.gitHubRepository + const checksRepository = this.getContributingRepository(repository) const checks = await this.getChecksForRef( checksRepository, @@ -330,6 +324,19 @@ export class NotificationsStore { this.statsStore.recordChecksFailedNotificationShown() } + private getContributingRepository( + repository: RepositoryWithGitHubRepository + ) { + const isForkContributingToParent = + isRepositoryWithForkedGitHubRepository(repository) && + repository.workflowPreferences.forkContributionTarget === + ForkContributionTarget.Parent + + return isForkContributingToParent + ? repository.gitHubRepository.parent + : repository.gitHubRepository + } + private isValidRepositoryForEvent( repository: RepositoryWithGitHubRepository, event: DesktopAliveEvent From ef396dbb4a6b422d3adc654cc30ef95f2a1b2e03 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 8 Nov 2022 16:09:35 -0500 Subject: [PATCH 171/262] Create dropdown suggested action component --- app/src/ui/changes/no-changes.tsx | 71 ++++++---- .../dropdown-suggested-action.tsx | 123 ++++++++++++++++++ 2 files changed, 170 insertions(+), 24 deletions(-) create mode 100644 app/src/ui/suggested-actions/dropdown-suggested-action.tsx diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index 43051d594b..97278675b5 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -22,6 +22,10 @@ import { Dispatcher } from '../dispatcher' import { SuggestedActionGroup } from '../suggested-actions' import { PreferencesTab } from '../../models/preferences' import { PopupType } from '../../models/popup' +import { + DropdownSuggestedAction, + IDropdownSuggestedActionOption, +} from '../suggested-actions/dropdown-suggested-action' function formatMenuItemLabel(text: string) { if (__WIN32__ || __LINUX__) { @@ -638,36 +642,55 @@ export class NoChanges extends React.Component< } private renderCreatePullRequestAction(tip: IValidBranch) { - const itemId: MenuIDs = 'create-pull-request' - const menuItem = this.getMenuItemInfo(itemId) - - if (menuItem === undefined) { - log.error(`Could not find matching menu item for ${itemId}`) + const createMenuItem = this.getMenuItemInfo('create-pull-request') + const startMenuItem = this.getMenuItemInfo('start-pull-request') + if (createMenuItem === undefined || startMenuItem === undefined) { + log.error( + `Could not find matching menu item for 'create-pull-request' or 'start-pull-request'` + ) return null } - const description = ( - <> - The current branch ({tip.branch.name}) is already published - to GitHub. Create a pull request to propose and collaborate on your - changes. - - ) - - const title = `Create a Pull Request from your current branch` - const buttonText = `Create Pull Request` + const pullRequestActions: ReadonlyArray = [ + { + title: `Preview the Pull Request from your current branch`, + label: 'Preview Pull Request', + description: ( + <> + The current branch ({tip.branch.name}) is already + published to GitHub. Create a pull request to propose and + collaborate on your changes. + + ), + value: 'PreviewPullRequest', + menuItemId: 'start-pull-request', + discoverabilityContent: + this.renderDiscoverabilityElements(startMenuItem), + disabled: !createMenuItem.enabled, + }, + { + title: `Create a Pull Request from your current branch`, + label: 'Create Pull Request', + description: ( + <> + The current branch ({tip.branch.name}) is already + published to GitHub. Preview the changes this pull request will + have. + + ), + value: 'CreatePullRequest', + menuItemId: 'create-pull-request', + discoverabilityContent: + this.renderDiscoverabilityElements(createMenuItem), + disabled: !createMenuItem.enabled, + onClick: this.onCreatePullRequestClicked, + }, + ] return ( - ) } diff --git a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx new file mode 100644 index 0000000000..6ff86b156a --- /dev/null +++ b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx @@ -0,0 +1,123 @@ +import * as React from 'react' +import { + DropdownSelectButton, + IDropdownSelectButtonOption, +} from '../dropdown-select-button' +import { MenuIDs } from '../../models/menu-ids' +import { executeMenuItemById } from '../main-process-proxy' + +export interface IDropdownSuggestedActionOption + extends IDropdownSelectButtonOption { + /** + * 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) => 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 { + readonly suggestedActions: ReadonlyArray +} + +interface IDropdownSuggestedActionState { + readonly selectedAction: IDropdownSuggestedActionOption +} + +export class DropdownSuggestedAction extends React.Component< + IDropdownSuggestedActionProps, + IDropdownSuggestedActionState +> { + public constructor(props: IDropdownSuggestedActionProps) { + super(props) + + this.state = { + selectedAction: this.props.suggestedActions[0], + } + } + + private onActionSelectionChange = (option: IDropdownSelectButtonOption) => { + const selectedAction = this.props.suggestedActions.find( + a => a.value === option.value + ) + if (selectedAction === undefined) { + return + } + + this.setState({ selectedAction }) + } + + private onActionSubmitted = (e: React.MouseEvent) => { + const { onClick, menuItemId } = this.state.selectedAction + onClick?.(e) + + if (!e.defaultPrevented && menuItemId !== undefined) { + executeMenuItemById(menuItemId) + } + } + + public render() { + const { + description, + image, + discoverabilityContent, + disabled, + value, + title, + } = this.state.selectedAction + + return ( +
+ {image &&
{image}
} +
+

{title}

+ {description ??

{description}

} + {discoverabilityContent && ( +

{discoverabilityContent}

+ )} +
+ ({ + label, + value, + }))} + disabled={disabled} + onSelectChange={this.onActionSelectionChange} + onSubmit={this.onActionSubmitted} + /> +
+ ) + } +} From 094abc50d7926868705e8e4a18a1ffb82be3c983 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 9 Nov 2022 08:29:49 -0500 Subject: [PATCH 172/262] && not ?? for description --- app/src/ui/suggested-actions/dropdown-suggested-action.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx index 6ff86b156a..505cd868d3 100644 --- a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx +++ b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx @@ -102,7 +102,7 @@ export class DropdownSuggestedAction extends React.Component< {image &&
{image}
}

{title}

- {description ??

{description}

} + {description &&

{description}

} {discoverabilityContent && (

{discoverabilityContent}

)} From 8e23e72c43fabaeb6f219a5c5dd625af4176a239 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 9 Nov 2022 08:54:51 -0500 Subject: [PATCH 173/262] Tweak styles of dropdown button --- app/styles/ui/_dropdown-select-button.scss | 5 +++-- app/styles/ui/suggested-actions/_suggested-action.scss | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/styles/ui/_dropdown-select-button.scss b/app/styles/ui/_dropdown-select-button.scss index a4d25cec09..a1dffd09c5 100644 --- a/app/styles/ui/_dropdown-select-button.scss +++ b/app/styles/ui/_dropdown-select-button.scss @@ -1,5 +1,6 @@ .dropdown-select-button { position: relative; + display: flex; &.open-bottom { .invoke-button { @@ -34,7 +35,6 @@ } .invoke-button { - width: 88%; display: inline; border-bottom-right-radius: 0; border-top-right-radius: 0; @@ -43,10 +43,11 @@ height: 30px; // counter balances center for the 12% dropdown button padding-left: 12% !important; + flex-grow: 1; } .dropdown-button { - width: 12%; + min-width: 30px; padding: var(--spacing-half); margin: 0; border-bottom-left-radius: 0; diff --git a/app/styles/ui/suggested-actions/_suggested-action.scss b/app/styles/ui/suggested-actions/_suggested-action.scss index 00f0846ddd..051d44082a 100644 --- a/app/styles/ui/suggested-actions/_suggested-action.scss +++ b/app/styles/ui/suggested-actions/_suggested-action.scss @@ -73,6 +73,10 @@ flex-shrink: 0; } + .dropdown-select-button { + padding: 0px var(--spacing-double); + } + /** Switch to a vertical layout at 1.25x zoom and above **/ @media screen and (max-width: 768px) { flex-direction: column; From a4267c2f0b0332f77f45ebf114ffebdbba8ae94f Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 9 Nov 2022 11:30:10 -0500 Subject: [PATCH 174/262] Remember users choice of PR suggested next action --- app/src/lib/app-state.ts | 10 ++++- app/src/lib/stores/app-store.ts | 30 ++++++++++++++- app/src/models/pull-request.ts | 9 +++++ app/src/ui/app.tsx | 1 + app/src/ui/changes/no-changes.tsx | 16 +++++++- app/src/ui/dispatcher/dispatcher.ts | 15 +++++++- app/src/ui/dropdown-select-button.tsx | 2 +- app/src/ui/repository.tsx | 7 ++++ .../dropdown-suggested-action.tsx | 37 ++++++++++++++++++- 9 files changed, 119 insertions(+), 8 deletions(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index ac2ebb07ce..ad8ca09a25 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -11,7 +11,10 @@ import { IMenu } from '../models/app-menu' import { IRemote } from '../models/remote' import { CloneRepositoryTab } from '../models/clone-repository-tab' import { BranchesTab } from '../models/branches-tab' -import { PullRequest } from '../models/pull-request' +import { + PullRequest, + PullRequestSuggestedNextAction, +} from '../models/pull-request' import { IAuthor } from '../models/author' import { MergeTreeResult } from '../models/merge' import { ICommitMessage } from '../models/commit-message' @@ -311,6 +314,11 @@ export interface IAppState { * Whether or not the user enabled high-signal notifications. */ readonly notificationsEnabled: boolean + + /** The users last chosen of pull request suggested next action. */ + readonly pullRequestSuggestedNextAction: + | PullRequestSuggestedNextAction + | undefined } export enum FoldoutType { diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 93e55ad513..728af6d5a5 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -29,7 +29,11 @@ import { GitHubRepository, hasWritePermission, } from '../../models/github-repository' -import { PullRequest } from '../../models/pull-request' +import { + defaultPullRequestSuggestedNextAction, + PullRequest, + PullRequestSuggestedNextAction, +} from '../../models/pull-request' import { forkPullRequestRemoteName, IRemote, @@ -383,6 +387,9 @@ const MaxInvalidFoldersToDisplay = 3 const lastThankYouKey = 'version-and-users-of-last-thank-you' const customThemeKey = 'custom-theme-key' +const pullRequestSuggestedNextActionKey = + 'pull-request-suggested-next-action-key' + export class AppStore extends TypedBaseStore { private readonly gitStoreCache: GitStoreCache @@ -506,6 +513,10 @@ export class AppStore extends TypedBaseStore { /** A service for managing the stack of open popups */ private popupManager = new PopupManager() + private pullRequestSuggestedNextAction: + | PullRequestSuggestedNextAction + | undefined = undefined + public constructor( private readonly gitHubUserStore: GitHubUserStore, private readonly cloningRepositoriesStore: CloningRepositoriesStore, @@ -968,6 +979,7 @@ export class AppStore extends TypedBaseStore { lastThankYou: this.lastThankYou, showCIStatusPopover: this.showCIStatusPopover, notificationsEnabled: getNotificationsEnabled(), + pullRequestSuggestedNextAction: this.pullRequestSuggestedNextAction, } } @@ -2084,6 +2096,12 @@ export class AppStore extends TypedBaseStore { this.lastThankYou = getObject(lastThankYouKey) + this.pullRequestSuggestedNextAction = + getEnum( + pullRequestSuggestedNextActionKey, + PullRequestSuggestedNextAction + ) ?? defaultPullRequestSuggestedNextAction + this.emitUpdateNow() this.accountsStore.refresh() @@ -7520,6 +7538,16 @@ export class AppStore extends TypedBaseStore { public _cancelQuittingApp() { sendCancelQuittingSync() } + + public _setPullRequestSuggestedNextAction( + value: PullRequestSuggestedNextAction + ) { + this.pullRequestSuggestedNextAction = value + + localStorage.setItem(pullRequestSuggestedNextActionKey, value) + + this.emitUpdate() + } } /** diff --git a/app/src/models/pull-request.ts b/app/src/models/pull-request.ts index 47dd05355a..76c186c551 100644 --- a/app/src/models/pull-request.ts +++ b/app/src/models/pull-request.ts @@ -41,3 +41,12 @@ export class PullRequest { public readonly body: string ) {} } + +/** The types of pull request suggested next actions */ +export enum PullRequestSuggestedNextAction { + PreviewPullRequest = 'PreviewPullRequest', + CreatePullRequest = 'CreatePullRequest', +} + +export const defaultPullRequestSuggestedNextAction = + PullRequestSuggestedNextAction.PreviewPullRequest diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index e3dc88cba3..86588db36d 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -3044,6 +3044,7 @@ export class App extends React.Component { aheadBehindStore={this.props.aheadBehindStore} commitSpellcheckEnabled={this.state.commitSpellcheckEnabled} onCherryPick={this.startCherryPickWithoutBranch} + pullRequestSuggestedNextAction={state.pullRequestSuggestedNextAction} /> ) } else if (selectedState.type === SelectionType.CloningRepository) { diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index 97278675b5..e86f42ffbe 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -26,6 +26,7 @@ import { DropdownSuggestedAction, IDropdownSuggestedActionOption, } from '../suggested-actions/dropdown-suggested-action' +import { PullRequestSuggestedNextAction } from '../../models/pull-request' function formatMenuItemLabel(text: string) { if (__WIN32__ || __LINUX__) { @@ -75,6 +76,9 @@ interface INoChangesProps { * opening the repository in an external editor. */ readonly isExternalEditorAvailable: boolean + + /** The user's preference of pull request suggested next action to use **/ + readonly pullRequestSuggestedNextAction?: PullRequestSuggestedNextAction } /** @@ -641,6 +645,12 @@ export class NoChanges extends React.Component< ) } + private onPullRequestSuggestedActionChanged = (action: string) => { + this.props.dispatcher.setPullRequestSuggestedNextAction( + action as PullRequestSuggestedNextAction + ) + } + private renderCreatePullRequestAction(tip: IValidBranch) { const createMenuItem = this.getMenuItemInfo('create-pull-request') const startMenuItem = this.getMenuItemInfo('start-pull-request') @@ -662,7 +672,7 @@ export class NoChanges extends React.Component< collaborate on your changes. ), - value: 'PreviewPullRequest', + value: PullRequestSuggestedNextAction.PreviewPullRequest, menuItemId: 'start-pull-request', discoverabilityContent: this.renderDiscoverabilityElements(startMenuItem), @@ -678,7 +688,7 @@ export class NoChanges extends React.Component< have. ), - value: 'CreatePullRequest', + value: PullRequestSuggestedNextAction.CreatePullRequest, menuItemId: 'create-pull-request', discoverabilityContent: this.renderDiscoverabilityElements(createMenuItem), @@ -691,6 +701,8 @@ export class NoChanges extends React.Component< ) } diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index 99917462c8..f85bf4e766 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -68,7 +68,10 @@ import { FetchType } from '../../models/fetch' import { GitHubRepository } from '../../models/github-repository' import { ManualConflictResolution } from '../../models/manual-conflict-resolution' import { Popup, PopupType } from '../../models/popup' -import { PullRequest } from '../../models/pull-request' +import { + PullRequest, + PullRequestSuggestedNextAction, +} from '../../models/pull-request' import { Repository, RepositoryWithGitHubRepository, @@ -4031,4 +4034,14 @@ export class Dispatcher { 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) + } } diff --git a/app/src/ui/dropdown-select-button.tsx b/app/src/ui/dropdown-select-button.tsx index f81aced6b0..e55f94a483 100644 --- a/app/src/ui/dropdown-select-button.tsx +++ b/app/src/ui/dropdown-select-button.tsx @@ -12,7 +12,7 @@ export interface IDropdownSelectButtonOption { readonly description?: string | JSX.Element /** The select option's value */ - readonly value?: string + readonly value: string } interface IDropdownSelectButtonProps { diff --git a/app/src/ui/repository.tsx b/app/src/ui/repository.tsx index 10339c12ac..2b0d4f8284 100644 --- a/app/src/ui/repository.tsx +++ b/app/src/ui/repository.tsx @@ -32,6 +32,7 @@ import { AheadBehindStore } from '../lib/stores/ahead-behind-store' import { dragAndDropManager } from '../lib/drag-and-drop-manager' import { DragType } from '../models/drag-drop' import { clamp } from '../lib/clamp' +import { PullRequestSuggestedNextAction } from '../models/pull-request' interface IRepositoryViewProps { readonly repository: Repository @@ -92,6 +93,9 @@ interface IRepositoryViewProps { repository: Repository, commits: ReadonlyArray ) => void + + /** The user's preference of pull request suggested next action to use **/ + readonly pullRequestSuggestedNextAction?: PullRequestSuggestedNextAction } interface IRepositoryViewState { @@ -465,6 +469,9 @@ export class RepositoryView extends React.Component< this.props.externalEditorLabel !== undefined } dispatcher={this.props.dispatcher} + pullRequestSuggestedNextAction={ + this.props.pullRequestSuggestedNextAction + } /> ) } diff --git a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx index 505cd868d3..a4014b55e0 100644 --- a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx +++ b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx @@ -5,6 +5,7 @@ import { } from '../dropdown-select-button' import { MenuIDs } from '../../models/menu-ids' import { executeMenuItemById } from '../main-process-proxy' +import { sendNonFatalException } from '../../lib/helpers/non-fatal-exception' export interface IDropdownSuggestedActionOption extends IDropdownSelectButtonOption { @@ -48,7 +49,16 @@ export interface IDropdownSuggestedActionOption } export interface IDropdownSuggestedActionProps { + /** The possible suggested next actions to select from + * + * This component assumes this is not an empty array. + */ readonly suggestedActions: ReadonlyArray + + /** The value of the selected next action to initialize the component with */ + readonly selectedActionValue?: string | undefined + + readonly onSuggestedActionChanged: (action: string) => void } interface IDropdownSuggestedActionState { @@ -62,8 +72,16 @@ export class DropdownSuggestedAction extends React.Component< public constructor(props: IDropdownSuggestedActionProps) { 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: this.props.suggestedActions[0], + selectedAction, } } @@ -71,11 +89,13 @@ export class DropdownSuggestedAction extends React.Component< 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) => { @@ -88,6 +108,18 @@ export class DropdownSuggestedAction extends React.Component< } 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, @@ -95,7 +127,8 @@ export class DropdownSuggestedAction extends React.Component< disabled, value, title, - } = this.state.selectedAction + } = selectedAction + return (
From 22f7553dcc153dafb5b6b508608c968df61b1611 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 9 Nov 2022 11:31:58 -0500 Subject: [PATCH 175/262] Tweak Css --- app/src/ui/changes/no-changes.tsx | 3 +- app/src/ui/dropdown-select-button.tsx | 36 ++++++++++--------- .../dropdown-suggested-action.tsx | 14 +++++++- app/styles/ui/_dropdown-select-button.scss | 8 ++--- .../ui/changes/_changes-interstitial.scss | 8 +++++ .../suggested-actions/_suggested-action.scss | 4 --- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index e86f42ffbe..1ef01e8862 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -699,7 +699,8 @@ export class NoChanges extends React.Component< return ( - - +
+ + +
{this.renderSplitButtonOptions()}
) diff --git a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx index a4014b55e0..0d2902fd1f 100644 --- a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx +++ b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx @@ -6,6 +6,7 @@ import { 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 extends IDropdownSelectButtonOption { @@ -59,6 +60,12 @@ export interface IDropdownSuggestedActionProps { readonly selectedActionValue?: string | undefined readonly onSuggestedActionChanged: (action: string) => 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 { @@ -129,9 +136,14 @@ export class DropdownSuggestedAction extends React.Component< title, } = selectedAction + const className = classNames( + 'suggested-action', + 'primary', + this.props.className + ) return ( -
+
{image &&
{image}
}

{title}

diff --git a/app/styles/ui/_dropdown-select-button.scss b/app/styles/ui/_dropdown-select-button.scss index a1dffd09c5..67261320ca 100644 --- a/app/styles/ui/_dropdown-select-button.scss +++ b/app/styles/ui/_dropdown-select-button.scss @@ -1,6 +1,9 @@ .dropdown-select-button { position: relative; - display: flex; + + .dropdown-button-wrappers { + display: flex; + } &.open-bottom { .invoke-button { @@ -39,10 +42,7 @@ border-bottom-right-radius: 0; border-top-right-radius: 0; margin-right: 0; - float: left; height: 30px; - // counter balances center for the 12% dropdown button - padding-left: 12% !important; flex-grow: 1; } diff --git a/app/styles/ui/changes/_changes-interstitial.scss b/app/styles/ui/changes/_changes-interstitial.scss index b147fc15fc..d07cb1783c 100644 --- a/app/styles/ui/changes/_changes-interstitial.scss +++ b/app/styles/ui/changes/_changes-interstitial.scss @@ -45,6 +45,14 @@ max-width: 600px; } + .pull-request-action { + .dropdown-select-button { + .invoke-button { + min-width: 150px; + } + } + } + /** Lessen the padding at 1.5x zoom and above **/ @media screen and (max-width: 640px) { padding: var(--spacing-double); diff --git a/app/styles/ui/suggested-actions/_suggested-action.scss b/app/styles/ui/suggested-actions/_suggested-action.scss index 051d44082a..00f0846ddd 100644 --- a/app/styles/ui/suggested-actions/_suggested-action.scss +++ b/app/styles/ui/suggested-actions/_suggested-action.scss @@ -73,10 +73,6 @@ flex-shrink: 0; } - .dropdown-select-button { - padding: 0px var(--spacing-double); - } - /** Switch to a vertical layout at 1.25x zoom and above **/ @media screen and (max-width: 768px) { flex-direction: column; From f9c4452fe5c2a089b5e7b00a0c9636ce883f0ba6 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 9 Nov 2022 11:45:17 -0500 Subject: [PATCH 176/262] Tidying --- app/src/lib/app-state.ts | 2 +- app/src/ui/changes/no-changes.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index ad8ca09a25..69c32a28c9 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -315,7 +315,7 @@ export interface IAppState { */ readonly notificationsEnabled: boolean - /** The users last chosen of pull request suggested next action. */ + /** The users last chosen pull request suggested next action. */ readonly pullRequestSuggestedNextAction: | PullRequestSuggestedNextAction | undefined diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index 1ef01e8862..70a439bffa 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -668,8 +668,8 @@ export class NoChanges extends React.Component< description: ( <> The current branch ({tip.branch.name}) is already - published to GitHub. Create a pull request to propose and - collaborate on your changes. + published to GitHub. Preview the changes this pull request will have + before proposing your changes. ), value: PullRequestSuggestedNextAction.PreviewPullRequest, @@ -684,8 +684,8 @@ export class NoChanges extends React.Component< description: ( <> The current branch ({tip.branch.name}) is already - published to GitHub. Preview the changes this pull request will - have. + published to GitHub. Create a pull request to propose and + collaborate on your changes. ), value: PullRequestSuggestedNextAction.CreatePullRequest, From 0a8568c2cdd422f2a1a25bcadaf55321b98a8448 Mon Sep 17 00:00:00 2001 From: Logan MacLaren Date: Wed, 9 Nov 2022 12:42:36 -0500 Subject: [PATCH 177/262] Create SECURITY.md --- SECURITY.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..d9ca9342c3 --- /dev/null +++ b/SECURITY.md @@ -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. From e7cf4395dfbc7a37790e295039c2571847346db5 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 9 Nov 2022 13:10:56 -0500 Subject: [PATCH 178/262] Feature flag it --- app/src/ui/changes/no-changes.tsx | 107 +++++++++++++++++++----------- 1 file changed, 69 insertions(+), 38 deletions(-) diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index 70a439bffa..8d54e32b54 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -27,6 +27,7 @@ import { IDropdownSuggestedActionOption, } from '../suggested-actions/dropdown-suggested-action' import { PullRequestSuggestedNextAction } from '../../models/pull-request' +import { enableStartingPullRequests } from '../../lib/feature-flag' function formatMenuItemLabel(text: string) { if (__WIN32__ || __LINUX__) { @@ -653,48 +654,78 @@ export class NoChanges extends React.Component< private renderCreatePullRequestAction(tip: IValidBranch) { const createMenuItem = this.getMenuItemInfo('create-pull-request') - const startMenuItem = this.getMenuItemInfo('start-pull-request') - if (createMenuItem === undefined || startMenuItem === undefined) { - log.error( - `Could not find matching menu item for 'create-pull-request' or 'start-pull-request'` - ) + if (createMenuItem === undefined) { + log.error(`Could not find matching menu item for 'create-pull-request'`) return null } + const description = ( + <> + The current branch ({tip.branch.name}) is already published + to GitHub. Create a pull request to propose and collaborate on your + changes. + + ) + + const title = `Create a Pull Request from your current branch` + const buttonText = `Create Pull Request` + + if (!enableStartingPullRequests()) { + return ( + + ) + } + + const startMenuItem = this.getMenuItemInfo('start-pull-request') + + if (startMenuItem === undefined) { + log.error(`Could not find matching menu item for 'start-pull-request'`) + return null + } + + const createPullRequestAction: IDropdownSuggestedActionOption = { + title, + label: buttonText, + description, + value: PullRequestSuggestedNextAction.CreatePullRequest, + menuItemId: 'create-pull-request', + discoverabilityContent: + this.renderDiscoverabilityElements(createMenuItem), + disabled: !createMenuItem.enabled, + onClick: this.onCreatePullRequestClicked, + } + + const previewPullRequestAction: IDropdownSuggestedActionOption = { + title: `Preview the Pull Request from your current branch`, + label: 'Preview Pull Request', + description: ( + <> + The current branch ({tip.branch.name}) is already published + to GitHub. Preview the changes this pull request will have before + proposing your changes. + + ), + value: PullRequestSuggestedNextAction.PreviewPullRequest, + menuItemId: 'start-pull-request', + discoverabilityContent: this.renderDiscoverabilityElements(startMenuItem), + disabled: !createMenuItem.enabled, + } + const pullRequestActions: ReadonlyArray = [ - { - title: `Preview the Pull Request from your current branch`, - label: 'Preview Pull Request', - description: ( - <> - The current branch ({tip.branch.name}) is already - published to GitHub. Preview the changes this pull request will have - before proposing your changes. - - ), - value: PullRequestSuggestedNextAction.PreviewPullRequest, - menuItemId: 'start-pull-request', - discoverabilityContent: - this.renderDiscoverabilityElements(startMenuItem), - disabled: !createMenuItem.enabled, - }, - { - title: `Create a Pull Request from your current branch`, - label: 'Create Pull Request', - description: ( - <> - The current branch ({tip.branch.name}) is already - published to GitHub. Create a pull request to propose and - collaborate on your changes. - - ), - value: PullRequestSuggestedNextAction.CreatePullRequest, - menuItemId: 'create-pull-request', - discoverabilityContent: - this.renderDiscoverabilityElements(createMenuItem), - disabled: !createMenuItem.enabled, - onClick: this.onCreatePullRequestClicked, - }, + previewPullRequestAction, + createPullRequestAction as IDropdownSuggestedActionOption, ] return ( From baf61a9874281ca9a9e511318eaa6cdc965dbc5f Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 9 Nov 2022 13:54:01 -0500 Subject: [PATCH 179/262] preview-pull-request-availability --- app/src/lib/menu-update.ts | 13 ++++++++++++- app/src/main-process/menu/build-default-menu.ts | 8 ++++---- app/src/main-process/menu/menu-event.ts | 2 +- app/src/models/menu-ids.ts | 2 +- app/src/ui/app.tsx | 2 +- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/lib/menu-update.ts b/app/src/lib/menu-update.ts index 9f0c644679..05dd9cb0b8 100644 --- a/app/src/lib/menu-update.ts +++ b/app/src/lib/menu-update.ts @@ -11,6 +11,7 @@ import { updateMenuState as ipcUpdateMenuState } from '../ui/main-process-proxy' import { AppMenu, MenuItem } from '../models/app-menu' import { hasConflictedFiles } from './status' import { findContributionTargetDefaultBranch } from './branch' +import { enableStartingPullRequests } from './feature-flag' export interface IMenuItemState { readonly enabled?: boolean @@ -135,6 +136,7 @@ const allMenuIds: ReadonlyArray = [ 'clone-repository', 'about', 'create-pull-request', + ...(enableStartingPullRequests() ? ['preview-pull-request' as MenuIDs] : []), 'squash-and-merge-branch', ] @@ -291,6 +293,13 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder { 'create-pull-request', isHostedOnGitHub && !branchIsUnborn && !onDetachedHead ) + if (enableStartingPullRequests()) { + menuStateBuilder.setEnabled( + 'preview-pull-request', + !branchIsUnborn && !onDetachedHead + ) + } + menuStateBuilder.setEnabled( 'push', !branchIsUnborn && !onDetachedHead && !networkActionInProgress @@ -330,7 +339,9 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder { menuStateBuilder.disable('view-repository-on-github') menuStateBuilder.disable('create-pull-request') - + if (enableStartingPullRequests()) { + menuStateBuilder.disable('preview-pull-request') + } if ( selectedState && selectedState.type === SelectionType.MissingRepository diff --git a/app/src/main-process/menu/build-default-menu.ts b/app/src/main-process/menu/build-default-menu.ts index 72640df091..30e1cdb5bb 100644 --- a/app/src/main-process/menu/build-default-menu.ts +++ b/app/src/main-process/menu/build-default-menu.ts @@ -434,12 +434,12 @@ export function buildDefaultMenu({ }, ] - if (!hasCurrentPullRequest && enableStartingPullRequests()) { + if (enableStartingPullRequests()) { branchSubmenu.push({ - label: __DARWIN__ ? 'Start Pull Request' : 'Start pull request', - id: 'start-pull-request', + label: __DARWIN__ ? 'Preview Pull Request' : 'Preview pull request', + id: 'preview-pull-request', accelerator: 'CmdOrCtrl+Alt+P', - click: emit('start-pull-request'), + click: emit('preview-pull-request'), }) } diff --git a/app/src/main-process/menu/menu-event.ts b/app/src/main-process/menu/menu-event.ts index 0a14ed6ce3..f71512cd71 100644 --- a/app/src/main-process/menu/menu-event.ts +++ b/app/src/main-process/menu/menu-event.ts @@ -43,5 +43,5 @@ export type MenuEvent = | 'find-text' | 'create-issue-in-repository-on-github' | 'pull-request-check-run-failed' - | 'start-pull-request' + | 'preview-pull-request' | 'show-app-error' diff --git a/app/src/models/menu-ids.ts b/app/src/models/menu-ids.ts index ebcb73d8c4..fd7449b1f6 100644 --- a/app/src/models/menu-ids.ts +++ b/app/src/models/menu-ids.ts @@ -35,4 +35,4 @@ export type MenuIDs = | 'compare-to-branch' | 'toggle-stashed-changes' | 'create-issue-in-repository-on-github' - | 'start-pull-request' + | 'preview-pull-request' diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index e3dc88cba3..ffceb35db5 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -437,7 +437,7 @@ export class App extends React.Component { return this.goToCommitMessage() case 'open-pull-request': return this.openPullRequest() - case 'start-pull-request': + case 'preview-pull-request': return this.startPullRequest() case 'install-cli': return this.props.dispatcher.installCLI() From 33e0da60cbb6c9e578530b28e362843ae1c157b1 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 14 Nov 2022 09:57:57 -0500 Subject: [PATCH 180/262] Typing the actions to avoid casting --- app/src/ui/changes/no-changes.tsx | 12 ++++++------ .../suggested-actions/dropdown-suggested-action.tsx | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index 8d54e32b54..6f93185951 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -646,10 +646,10 @@ export class NoChanges extends React.Component< ) } - private onPullRequestSuggestedActionChanged = (action: string) => { - this.props.dispatcher.setPullRequestSuggestedNextAction( - action as PullRequestSuggestedNextAction - ) + private onPullRequestSuggestedActionChanged = ( + action: PullRequestSuggestedNextAction + ) => { + this.props.dispatcher.setPullRequestSuggestedNextAction(action) } private renderCreatePullRequestAction(tip: IValidBranch) { @@ -725,11 +725,11 @@ export class NoChanges extends React.Component< const pullRequestActions: ReadonlyArray = [ previewPullRequestAction, - createPullRequestAction as IDropdownSuggestedActionOption, + createPullRequestAction, ] return ( - key="pull-request-action" className="pull-request-action" suggestedActions={pullRequestActions} diff --git a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx index 0d2902fd1f..144bef3d21 100644 --- a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx +++ b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx @@ -49,7 +49,7 @@ export interface IDropdownSuggestedActionOption readonly menuItemId?: MenuIDs } -export interface IDropdownSuggestedActionProps { +export interface IDropdownSuggestedActionProps { /** The possible suggested next actions to select from * * This component assumes this is not an empty array. @@ -57,9 +57,9 @@ export interface IDropdownSuggestedActionProps { readonly suggestedActions: ReadonlyArray /** The value of the selected next action to initialize the component with */ - readonly selectedActionValue?: string | undefined + readonly selectedActionValue?: T - readonly onSuggestedActionChanged: (action: string) => void + readonly onSuggestedActionChanged: (action: T) => void /** * An optional additional class name to set in order to be able to apply @@ -72,11 +72,11 @@ interface IDropdownSuggestedActionState { readonly selectedAction: IDropdownSuggestedActionOption } -export class DropdownSuggestedAction extends React.Component< - IDropdownSuggestedActionProps, +export class DropdownSuggestedAction extends React.Component< + IDropdownSuggestedActionProps, IDropdownSuggestedActionState > { - public constructor(props: IDropdownSuggestedActionProps) { + public constructor(props: IDropdownSuggestedActionProps) { super(props) const { selectedActionValue, suggestedActions } = props From 9531d3fadf76855ab0498ffcd23c4e962e111070 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 14 Nov 2022 10:13:48 -0500 Subject: [PATCH 181/262] Release the title id --- .../open-pull-request-header.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-header.tsx b/app/src/ui/open-pull-request/open-pull-request-header.tsx index b9e376387b..6bb7bc97fd 100644 --- a/app/src/ui/open-pull-request/open-pull-request-header.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-header.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { Branch } from '../../models/branch' import { BranchSelect } from '../branches/branch-select' import { DialogHeader } from '../dialog/header' -import { createUniqueId } from '../lib/id-pool' +import { createUniqueId, releaseUniqueId } from '../lib/id-pool' import { Ref } from '../lib/ref' interface IOpenPullRequestDialogHeaderProps { @@ -40,14 +40,33 @@ interface IOpenPullRequestDialogHeaderProps { readonly onDismissed?: () => void } +interface IOpenPullRequestDialogHeaderState { + /** + * An id for the h1 element that contains the title of this dialog. Used to + * aid in accessibility by allowing the h1 to be referenced in an + * aria-labeledby/aria-describedby attributed. Undefined if the dialog does + * not have a title or the component has not yet been mounted. + */ + readonly titleId: string +} + /** * A header component for the open pull request dialog. Made to house the * base branch dropdown and merge details common to all pull request views. */ export class OpenPullRequestDialogHeader extends React.Component< IOpenPullRequestDialogHeaderProps, - {} + IOpenPullRequestDialogHeaderState > { + public constructor(props: IOpenPullRequestDialogHeaderProps) { + super(props) + this.state = { titleId: createUniqueId(`Dialog_Open_Pull_Request`) } + } + + public componentWillUnmount() { + releaseUniqueId(this.state.titleId) + } + public render() { const title = __DARWIN__ ? 'Open a Pull Request' : 'Open a pull request' const { @@ -65,7 +84,7 @@ export class OpenPullRequestDialogHeader extends React.Component< return ( From da03577c22b24272b4e0e1d392689b1c4f8b268f Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 14 Nov 2022 10:28:59 -0500 Subject: [PATCH 182/262] Type all the way down to dropdown component --- app/src/ui/changes/no-changes.tsx | 62 ++++++++++--------- app/src/ui/dropdown-select-button.tsx | 36 ++++++----- .../dropdown-suggested-action.tsx | 24 ++++--- 3 files changed, 66 insertions(+), 56 deletions(-) diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index 6f93185951..c74392a584 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -695,38 +695,40 @@ export class NoChanges extends React.Component< return null } - const createPullRequestAction: IDropdownSuggestedActionOption = { - title, - label: buttonText, - description, - value: PullRequestSuggestedNextAction.CreatePullRequest, - menuItemId: 'create-pull-request', - discoverabilityContent: - this.renderDiscoverabilityElements(createMenuItem), - disabled: !createMenuItem.enabled, - onClick: this.onCreatePullRequestClicked, - } + const createPullRequestAction: IDropdownSuggestedActionOption = + { + title, + label: buttonText, + description, + value: PullRequestSuggestedNextAction.CreatePullRequest, + menuItemId: 'create-pull-request', + discoverabilityContent: + this.renderDiscoverabilityElements(createMenuItem), + disabled: !createMenuItem.enabled, + onClick: this.onCreatePullRequestClicked, + } - const previewPullRequestAction: IDropdownSuggestedActionOption = { - title: `Preview the Pull Request from your current branch`, - label: 'Preview Pull Request', - description: ( - <> - The current branch ({tip.branch.name}) is already published - to GitHub. Preview the changes this pull request will have before - proposing your changes. - - ), - value: PullRequestSuggestedNextAction.PreviewPullRequest, - menuItemId: 'start-pull-request', - discoverabilityContent: this.renderDiscoverabilityElements(startMenuItem), - disabled: !createMenuItem.enabled, - } + const previewPullRequestAction: IDropdownSuggestedActionOption = + { + title: `Preview the Pull Request from your current branch`, + label: 'Preview Pull Request', + description: ( + <> + The current branch ({tip.branch.name}) is already + published to GitHub. Preview the changes this pull request will have + before proposing your changes. + + ), + value: PullRequestSuggestedNextAction.PreviewPullRequest, + menuItemId: 'start-pull-request', + discoverabilityContent: + this.renderDiscoverabilityElements(startMenuItem), + disabled: !createMenuItem.enabled, + } - const pullRequestActions: ReadonlyArray = [ - previewPullRequestAction, - createPullRequestAction, - ] + const pullRequestActions: ReadonlyArray< + IDropdownSuggestedActionOption + > = [previewPullRequestAction, createPullRequestAction] return ( diff --git a/app/src/ui/dropdown-select-button.tsx b/app/src/ui/dropdown-select-button.tsx index dc56865817..fa6ba0da0d 100644 --- a/app/src/ui/dropdown-select-button.tsx +++ b/app/src/ui/dropdown-select-button.tsx @@ -4,7 +4,7 @@ import { Button } from './lib/button' import { Octicon } from './octicons' import * as OcticonSymbol from './octicons/octicons.generated' -export interface IDropdownSelectButtonOption { +export interface IDropdownSelectButtonOption { /** The select option header label. */ readonly label?: string | JSX.Element @@ -12,15 +12,15 @@ export interface IDropdownSelectButtonOption { readonly description?: string | JSX.Element /** The select option's value */ - readonly value: string + readonly value: T } -interface IDropdownSelectButtonProps { +interface IDropdownSelectButtonProps { /** The selection button options */ - readonly options: ReadonlyArray + readonly options: ReadonlyArray> /** The selection option value */ - readonly selectedValue?: string + readonly selectedValue?: T /** Whether or not the button is enabled */ readonly disabled?: boolean @@ -30,22 +30,22 @@ interface IDropdownSelectButtonProps { /** Callback for when the button selection changes*/ readonly onSelectChange?: ( - selectedOption: IDropdownSelectButtonOption + selectedOption: IDropdownSelectButtonOption ) => void /** Callback for when button is selected option button is clicked */ readonly onSubmit?: ( event: React.MouseEvent, - selectedOption: IDropdownSelectButtonOption + selectedOption: IDropdownSelectButtonOption ) => void } -interface IDropdownSelectButtonState { +interface IDropdownSelectButtonState { /** Whether the options are rendered */ readonly showButtonOptions: boolean /** The currently selected option */ - readonly selectedOption: IDropdownSelectButtonOption | null + readonly selectedOption: IDropdownSelectButtonOption | null /** * The adjusting position of the options popover. This is calculated based @@ -54,14 +54,16 @@ interface IDropdownSelectButtonState { readonly optionsPositionBottom?: string } -export class DropdownSelectButton extends React.Component< - IDropdownSelectButtonProps, - IDropdownSelectButtonState +export class DropdownSelectButton< + T extends string = string +> extends React.Component< + IDropdownSelectButtonProps, + IDropdownSelectButtonState > { private invokeButtonRef: HTMLButtonElement | null = null private optionsContainerRef: HTMLDivElement | null = null - public constructor(props: IDropdownSelectButtonProps) { + public constructor(props: IDropdownSelectButtonProps) { super(props) this.state = { @@ -90,8 +92,8 @@ export class DropdownSelectButton extends React.Component< } private getSelectedOption( - selectedValue: string | undefined - ): IDropdownSelectButtonOption | null { + selectedValue: T | undefined + ): IDropdownSelectButtonOption | null { const { options } = this.props if (options.length === 0) { return null @@ -104,7 +106,9 @@ export class DropdownSelectButton extends React.Component< return selectedOption } - private onSelectionChange = (selectedOption: IDropdownSelectButtonOption) => { + private onSelectionChange = ( + selectedOption: IDropdownSelectButtonOption + ) => { return (_event: React.MouseEvent) => { this.setState({ selectedOption, showButtonOptions: false }) diff --git a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx index 144bef3d21..60341265fa 100644 --- a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx +++ b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx @@ -8,8 +8,8 @@ import { executeMenuItemById } from '../main-process-proxy' import { sendNonFatalException } from '../../lib/helpers/non-fatal-exception' import classNames from 'classnames' -export interface IDropdownSuggestedActionOption - extends IDropdownSelectButtonOption { +export interface IDropdownSuggestedActionOption + extends IDropdownSelectButtonOption { /** * The title, or "header" text for a suggested * action. @@ -49,12 +49,12 @@ export interface IDropdownSuggestedActionOption readonly menuItemId?: MenuIDs } -export interface IDropdownSuggestedActionProps { +export interface IDropdownSuggestedActionProps { /** The possible suggested next actions to select from * * This component assumes this is not an empty array. */ - readonly suggestedActions: ReadonlyArray + readonly suggestedActions: ReadonlyArray> /** The value of the selected next action to initialize the component with */ readonly selectedActionValue?: T @@ -68,13 +68,15 @@ export interface IDropdownSuggestedActionProps { readonly className?: string } -interface IDropdownSuggestedActionState { - readonly selectedAction: IDropdownSuggestedActionOption +interface IDropdownSuggestedActionState { + readonly selectedAction: IDropdownSuggestedActionOption } -export class DropdownSuggestedAction extends React.Component< +export class DropdownSuggestedAction< + T extends string = string +> extends React.Component< IDropdownSuggestedActionProps, - IDropdownSuggestedActionState + IDropdownSuggestedActionState > { public constructor(props: IDropdownSuggestedActionProps) { super(props) @@ -92,7 +94,9 @@ export class DropdownSuggestedAction extends React.Component< } } - private onActionSelectionChange = (option: IDropdownSelectButtonOption) => { + private onActionSelectionChange = ( + option: IDropdownSelectButtonOption + ) => { const selectedAction = this.props.suggestedActions.find( a => a.value === option.value ) @@ -152,7 +156,7 @@ export class DropdownSuggestedAction extends React.Component<

{discoverabilityContent}

)}
- selectedValue={value} options={this.props.suggestedActions.map(({ label, value }) => ({ label, From f52efc6c9d1e61d62d1b6c153f6b43b2f9b0cc7d Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 14 Nov 2022 15:36:36 -0500 Subject: [PATCH 183/262] Turn options into buttons to be tabable --- app/src/ui/dropdown-select-button.tsx | 24 ++++++----- app/styles/ui/_dropdown-select-button.scss | 50 ++++++++++++++-------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/app/src/ui/dropdown-select-button.tsx b/app/src/ui/dropdown-select-button.tsx index f81aced6b0..d48c959a1d 100644 --- a/app/src/ui/dropdown-select-button.tsx +++ b/app/src/ui/dropdown-select-button.tsx @@ -105,7 +105,7 @@ export class DropdownSelectButton extends React.Component< } private onSelectionChange = (selectedOption: IDropdownSelectButtonOption) => { - return (_event: React.MouseEvent) => { + return (_event?: React.MouseEvent) => { this.setState({ selectedOption, showButtonOptions: false }) const { onSelectChange } = this.props @@ -141,6 +141,16 @@ export class DropdownSelectButton extends React.Component< ) } + private renderOption = (o: IDropdownSelectButtonOption, i: number) => { + return ( + + ) + } + private renderSplitButtonOptions() { if (!this.state.showButtonOptions) { return @@ -150,22 +160,14 @@ export class DropdownSelectButton extends React.Component< const { optionsPositionBottom: bottom } = this.state const openClass = bottom !== undefined ? 'open-top' : 'open-bottom' const classes = classNames('dropdown-select-button-options', openClass) + return (
-
    - {options.map(o => ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions -
  • - {this.renderSelectedIcon(o)} -
    {o.label}
    -
    {o.description}
    -
  • - ))} -
+ {options.map((o, i) => this.renderOption(o, i))}
) } diff --git a/app/styles/ui/_dropdown-select-button.scss b/app/styles/ui/_dropdown-select-button.scss index a4d25cec09..0602e7a4e4 100644 --- a/app/styles/ui/_dropdown-select-button.scss +++ b/app/styles/ui/_dropdown-select-button.scss @@ -85,29 +85,41 @@ box-shadow: var(--base-box-shadow); width: 99.9%; - ul { - padding: 0; + padding: 0; + margin: 0; + + .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; - li { - list-style-type: none; - padding: var(--spacing) var(--spacing-double); - padding-left: var(--spacing-triple); - border-bottom: 1px solid var(--box-border-color); - - .option-description { - color: var(--text-secondary-color); - font-size: var(--font-size-sm); - } - - .selected-option-indicator { - position: absolute; - left: var(--spacing); - } + &:hover, + &:focus { + outline: none; + color: var(--text-color); + background-color: var(--box-selected-background-color); } - li:hover { - background-color: var(--box-selected-background-color); + .option-description { + color: var(--text-secondary-color); + font-size: var(--font-size-sm); + } + + .selected-option-indicator { + position: absolute; + left: var(--spacing); } } } From 5dd914d85b877168dff06d9adaffc412ee9782cf Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 14 Nov 2022 17:11:06 -0500 Subject: [PATCH 184/262] Add keyboard navigation --- app/src/ui/dropdown-select-button.tsx | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/app/src/ui/dropdown-select-button.tsx b/app/src/ui/dropdown-select-button.tsx index d48c959a1d..ae9b3bd005 100644 --- a/app/src/ui/dropdown-select-button.tsx +++ b/app/src/ui/dropdown-select-button.tsx @@ -89,6 +89,56 @@ 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 || + !['ArrowUp', 'ArrowDown'].includes(key) + ) { + return + } + + const buttons = this.optionsContainerRef?.querySelectorAll( + '.dropdown-select-button-options .button-component' + ) + + if (buttons === undefined) { + return + } + + const foundCurrentIndex = [...buttons].findIndex(b => + b.className.includes('focus') + ) + + let focusedOptionIndex = -1 + if (foundCurrentIndex !== -1) { + if (key === 'ArrowUp') { + focusedOptionIndex = + foundCurrentIndex !== 0 + ? foundCurrentIndex - 1 + : this.props.options.length - 1 + } else { + focusedOptionIndex = + foundCurrentIndex !== this.props.options.length - 1 + ? foundCurrentIndex + 1 + : 0 + } + } else { + focusedOptionIndex = key === 'ArrowUp' ? this.props.options.length - 1 : 0 + } + + const button = buttons?.item(focusedOptionIndex) as HTMLButtonElement + button?.focus() + } + private getSelectedOption( selectedValue: string | undefined ): IDropdownSelectButtonOption | null { From b1e70a7f9d1367856f3a23363867300cbc1e3a8b Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 14 Nov 2022 17:25:32 -0500 Subject: [PATCH 185/262] Use selection styles for selection options with keyboard --- app/styles/ui/_dropdown-select-button.scss | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/styles/ui/_dropdown-select-button.scss b/app/styles/ui/_dropdown-select-button.scss index 0602e7a4e4..8caa6ac1f8 100644 --- a/app/styles/ui/_dropdown-select-button.scss +++ b/app/styles/ui/_dropdown-select-button.scss @@ -88,6 +88,10 @@ padding: 0; margin: 0; + :first-child.button-component { + border-top: 1px solid var(--box-border-color); + } + .button-component { padding: var(--spacing) var(--spacing-double); padding-left: var(--spacing-triple); @@ -108,8 +112,12 @@ &:hover, &:focus { outline: none; - color: var(--text-color); - background-color: var(--box-selected-background-color); + color: var(--box-selected-active-text-color); + background-color: var(--box-selected-active-background-color); + + .option-description { + color: var(--box-selected-active-text-color); + } } .option-description { From 483c8a38b66f9496e74352a7a45502c517a3d022 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 14 Nov 2022 17:48:39 -0500 Subject: [PATCH 186/262] Close buttons options on escape --- app/src/ui/dropdown-select-button.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/ui/dropdown-select-button.tsx b/app/src/ui/dropdown-select-button.tsx index ae9b3bd005..c5704fcfa7 100644 --- a/app/src/ui/dropdown-select-button.tsx +++ b/app/src/ui/dropdown-select-button.tsx @@ -99,6 +99,11 @@ export class DropdownSelectButton extends React.Component< 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) From 7fb49ffa5771a3c18dfd6a2c5af9402d4b2ae0e8 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 15 Nov 2022 10:33:16 +0100 Subject: [PATCH 187/262] Tidying types a bit --- app/src/ui/changes/no-changes.tsx | 8 ++------ app/src/ui/dropdown-select-button.tsx | 8 ++++---- .../merge-call-to-action-with-conflicts.tsx | 15 +++++++-------- app/src/ui/lib/update-branch.ts | 4 +++- .../choose-branch/base-choose-branch-dialog.tsx | 9 +++++---- .../dropdown-suggested-action.tsx | 10 ++++------ 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index c74392a584..377c035483 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -726,15 +726,11 @@ export class NoChanges extends React.Component< disabled: !createMenuItem.enabled, } - const pullRequestActions: ReadonlyArray< - IDropdownSuggestedActionOption - > = [previewPullRequestAction, createPullRequestAction] - return ( - + diff --git a/app/src/ui/dropdown-select-button.tsx b/app/src/ui/dropdown-select-button.tsx index fa6ba0da0d..29e873b766 100644 --- a/app/src/ui/dropdown-select-button.tsx +++ b/app/src/ui/dropdown-select-button.tsx @@ -4,7 +4,7 @@ import { Button } from './lib/button' import { Octicon } from './octicons' import * as OcticonSymbol from './octicons/octicons.generated' -export interface IDropdownSelectButtonOption { +export interface IDropdownSelectButtonOption { /** The select option header label. */ readonly label?: string | JSX.Element @@ -15,7 +15,7 @@ export interface IDropdownSelectButtonOption { readonly value: T } -interface IDropdownSelectButtonProps { +interface IDropdownSelectButtonProps { /** The selection button options */ readonly options: ReadonlyArray> @@ -40,7 +40,7 @@ interface IDropdownSelectButtonProps { ) => void } -interface IDropdownSelectButtonState { +interface IDropdownSelectButtonState { /** Whether the options are rendered */ readonly showButtonOptions: boolean @@ -131,7 +131,7 @@ export class DropdownSelectButton< this.optionsContainerRef = ref } - private renderSelectedIcon(option: IDropdownSelectButtonOption) { + private renderSelectedIcon(option: IDropdownSelectButtonOption) { const { selectedOption } = this.state if (selectedOption === null || option.value !== selectedOption.value) { return diff --git a/app/src/ui/history/merge-call-to-action-with-conflicts.tsx b/app/src/ui/history/merge-call-to-action-with-conflicts.tsx index 060bfe31b7..f717bba781 100644 --- a/app/src/ui/history/merge-call-to-action-with-conflicts.tsx +++ b/app/src/ui/history/merge-call-to-action-with-conflicts.tsx @@ -96,25 +96,24 @@ export class MergeCallToActionWithConflicts extends React.Component< }) } - private onOperationChange = (option: IDropdownSelectButtonOption) => { - const value = option.value as MultiCommitOperationKind - this.setState({ selectedOperation: value }) - if (value === MultiCommitOperationKind.Rebase) { + private onOperationChange = ( + option: IDropdownSelectButtonOption + ) => { + this.setState({ selectedOperation: option.value }) + if (option.value === MultiCommitOperationKind.Rebase) { this.updateRebasePreview(this.props.comparisonBranch) } } private onOperationInvoked = async ( event: React.MouseEvent, - selectedOption: IDropdownSelectButtonOption + selectedOption: IDropdownSelectButtonOption ) => { event.preventDefault() const { dispatcher, repository } = this.props - await this.dispatchOperation( - selectedOption.value as MultiCommitOperationKind - ) + await this.dispatchOperation(selectedOption.value) dispatcher.executeCompare(repository, { kind: HistoryTabMode.History, diff --git a/app/src/ui/lib/update-branch.ts b/app/src/ui/lib/update-branch.ts index 4c32641667..49bb59c8e2 100644 --- a/app/src/ui/lib/update-branch.ts +++ b/app/src/ui/lib/update-branch.ts @@ -7,7 +7,9 @@ import { RebasePreview } from '../../models/rebase' import { Repository } from '../../models/repository' import { IDropdownSelectButtonOption } from '../dropdown-select-button' -export function getMergeOptions(): ReadonlyArray { +export function getMergeOptions(): ReadonlyArray< + IDropdownSelectButtonOption +> { return [ { label: 'Create a merge commit', diff --git a/app/src/ui/multi-commit-operation/choose-branch/base-choose-branch-dialog.tsx b/app/src/ui/multi-commit-operation/choose-branch/base-choose-branch-dialog.tsx index dedda9490c..1b31d8deb3 100644 --- a/app/src/ui/multi-commit-operation/choose-branch/base-choose-branch-dialog.tsx +++ b/app/src/ui/multi-commit-operation/choose-branch/base-choose-branch-dialog.tsx @@ -161,11 +161,12 @@ export abstract class BaseChooseBranchDialog extends React.Component< return currentBranch === defaultBranch ? null : defaultBranch } - private onOperationChange = (option: IDropdownSelectButtonOption) => { - const value = option.value as MultiCommitOperationKind + private onOperationChange = ( + option: IDropdownSelectButtonOption + ) => { const { dispatcher, repository } = this.props const { selectedBranch } = this.state - switch (value) { + switch (option.value) { case MultiCommitOperationKind.Merge: dispatcher.startMergeBranchOperation(repository, false, selectedBranch) break @@ -179,7 +180,7 @@ export abstract class BaseChooseBranchDialog extends React.Component< case MultiCommitOperationKind.Reorder: break default: - assertNever(value, `Unknown operation value: ${option.value}`) + assertNever(option.value, `Unknown operation value: ${option.value}`) } } diff --git a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx index 60341265fa..57eefdb6ce 100644 --- a/app/src/ui/suggested-actions/dropdown-suggested-action.tsx +++ b/app/src/ui/suggested-actions/dropdown-suggested-action.tsx @@ -8,7 +8,7 @@ import { executeMenuItemById } from '../main-process-proxy' import { sendNonFatalException } from '../../lib/helpers/non-fatal-exception' import classNames from 'classnames' -export interface IDropdownSuggestedActionOption +export interface IDropdownSuggestedActionOption extends IDropdownSelectButtonOption { /** * The title, or "header" text for a suggested @@ -49,7 +49,7 @@ export interface IDropdownSuggestedActionOption readonly menuItemId?: MenuIDs } -export interface IDropdownSuggestedActionProps { +export interface IDropdownSuggestedActionProps { /** The possible suggested next actions to select from * * This component assumes this is not an empty array. @@ -68,13 +68,11 @@ export interface IDropdownSuggestedActionProps { readonly className?: string } -interface IDropdownSuggestedActionState { +interface IDropdownSuggestedActionState { readonly selectedAction: IDropdownSuggestedActionOption } -export class DropdownSuggestedAction< - T extends string = string -> extends React.Component< +export class DropdownSuggestedAction extends React.Component< IDropdownSuggestedActionProps, IDropdownSuggestedActionState > { From e2f35022127633db8e056aa3a7f722ce430ef017 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 15 Nov 2022 08:45:55 -0500 Subject: [PATCH 188/262] merge didn't pull right menu items? --- app/src/ui/changes/no-changes.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index 377c035483..b8aad1cc84 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -688,10 +688,10 @@ export class NoChanges extends React.Component< ) } - const startMenuItem = this.getMenuItemInfo('start-pull-request') + const startMenuItem = this.getMenuItemInfo('preview-pull-request') if (startMenuItem === undefined) { - log.error(`Could not find matching menu item for 'start-pull-request'`) + log.error(`Could not find matching menu item for 'preview-pull-request'`) return null } @@ -720,7 +720,7 @@ export class NoChanges extends React.Component< ), value: PullRequestSuggestedNextAction.PreviewPullRequest, - menuItemId: 'start-pull-request', + menuItemId: 'preview-pull-request', discoverabilityContent: this.renderDiscoverabilityElements(startMenuItem), disabled: !createMenuItem.enabled, From e7830a9866dbf8cc5e118d9e5e711d8bf1046bb1 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 15 Nov 2022 08:50:54 -0500 Subject: [PATCH 189/262] Make sure buttons take entire width up --- app/styles/ui/_dropdown-select-button.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/ui/_dropdown-select-button.scss b/app/styles/ui/_dropdown-select-button.scss index 9a35f3cce5..c720e05432 100644 --- a/app/styles/ui/_dropdown-select-button.scss +++ b/app/styles/ui/_dropdown-select-button.scss @@ -109,6 +109,7 @@ border-right: 0; border-left: 0; border-top: 0; + width: 100%; &:hover, &:focus { From 0b711717fdff0a295080a567885a215a7c2ba883 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Tue, 15 Nov 2022 17:34:35 +0100 Subject: [PATCH 190/262] Fix logic error in onRowMouseOver Turns out https://github.com/desktop/desktop/commit/e11038a04fec292649bb1f9a0b427fd7fca8b64d introduced a regression where the intention was changed from "selection not included" to "selection included" --- app/src/ui/lib/list/list.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/ui/lib/list/list.tsx b/app/src/ui/lib/list/list.tsx index 4ff4c06fa9..c3aa3bdba2 100644 --- a/app/src/ui/lib/list/list.tsx +++ b/app/src/ui/lib/list/list.tsx @@ -588,11 +588,8 @@ export class List extends React.Component { private onRowMouseOver = (row: number, event: React.MouseEvent) => { if (this.props.selectOnHover && this.canSelectRow(row)) { - if ( - this.props.selectedRows.includes(row) && - this.props.onSelectionChanged - ) { - this.props.onSelectionChanged([row], { kind: 'hover', event }) + if (!this.props.selectedRows.includes(row)) { + this.props.onSelectionChanged?.([row], { kind: 'hover', event }) // By calling scrollRowToVisible we ensure that hovering over a partially // visible item at the top or bottom of the list scrolls it into view but // more importantly `scrollRowToVisible` automatically manages focus so From 017e3b1211853a5cc6efcacb792f7c2884fea5c6 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 15 Nov 2022 13:11:34 -0500 Subject: [PATCH 191/262] Release 3.1.3-beta3 --- app/package.json | 2 +- changelog.json | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index e22e0975dc..b5ea4b262c 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "productName": "GitHub Desktop", "bundleID": "com.github.GitHubClient", "companyName": "GitHub, Inc.", - "version": "3.1.3-beta2", + "version": "3.1.3-beta3", "main": "./main.js", "repository": { "type": "git", diff --git a/changelog.json b/changelog.json index 2562d8c5ba..4b873fa690 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,16 @@ { "releases": { + "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" + ], "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", From c165d3e4a9249d7b029bdabaaf7d38d3c4c0e49a Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 15 Nov 2022 13:33:07 -0500 Subject: [PATCH 192/262] Enabled stacked popovers for beta --- app/src/lib/feature-flag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/feature-flag.ts b/app/src/lib/feature-flag.ts index fed2808adb..67c445ba3e 100644 --- a/app/src/lib/feature-flag.ts +++ b/app/src/lib/feature-flag.ts @@ -115,7 +115,7 @@ export function enableStartingPullRequests(): boolean { /** Should we enable starting pull requests? */ export function enableStackedPopups(): boolean { - return enableDevelopmentFeatures() + return enableBetaFeatures() } /** Should we enable mechanism to prevent closing while the app is updating? */ From 93438ed8de6acef49e273ffaac9560af3e678c5b Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 15 Nov 2022 13:38:20 -0500 Subject: [PATCH 193/262] Stacked popup note --- changelog.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.json b/changelog.json index 4b873fa690..4c3a01ab37 100644 --- a/changelog.json +++ b/changelog.json @@ -9,7 +9,8 @@ "[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] 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", From 39c3e4566ed9e08d9322250dabc6f13304dddc25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 22:17:05 +0000 Subject: [PATCH 194/262] Bump loader-utils from 1.4.1 to 1.4.2 Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4c8ec8babf..d521ba5396 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6676,9 +6676,9 @@ loader-runner@^4.2.0: integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== loader-utils@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.1.tgz#278ad7006660bccc4d2c0c1578e17c5c78d5c0e0" - integrity sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q== + version "1.4.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" From dcec086a18cb9c23a49f45371b61ac9ae917804d Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 16 Nov 2022 11:17:46 +0100 Subject: [PATCH 195/262] Use `getForkContributionTarget` :unamused: --- app/src/lib/stores/notifications-store.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index 64963f68ea..880c882681 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -3,6 +3,7 @@ import { isRepositoryWithGitHubRepository, RepositoryWithGitHubRepository, isRepositoryWithForkedGitHubRepository, + getForkContributionTarget, } from '../../models/repository' import { ForkContributionTarget } from '../../models/workflow-preferences' import { getPullRequestCommitRef, PullRequest } from '../../models/pull-request' @@ -329,8 +330,7 @@ export class NotificationsStore { ) { const isForkContributingToParent = isRepositoryWithForkedGitHubRepository(repository) && - repository.workflowPreferences.forkContributionTarget === - ForkContributionTarget.Parent + getForkContributionTarget(repository) === ForkContributionTarget.Parent return isForkContributingToParent ? repository.gitHubRepository.parent @@ -345,8 +345,7 @@ export class NotificationsStore { // match the parent repository. if ( isRepositoryWithForkedGitHubRepository(repository) && - repository.workflowPreferences.forkContributionTarget === - ForkContributionTarget.Parent + getForkContributionTarget(repository) === ForkContributionTarget.Parent ) { const parentRepository = repository.gitHubRepository.parent return ( From 320b9264e199ab67f22bb9317ee6795af549f442 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 16 Nov 2022 12:18:30 +0100 Subject: [PATCH 196/262] Skip known check RUNS instead of check SUITES --- app/src/lib/stores/notifications-store.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index 880c882681..419f8fb196 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -75,7 +75,7 @@ export class NotificationsStore { null private cachedCommits: Map = new Map() private skipCommitShas: Set = new Set() - private skipCheckSuites: Set = new Set() + private skipCheckRuns: Set = new Set() public constructor( private readonly accountsStore: AccountsStore, @@ -216,10 +216,6 @@ export class NotificationsStore { return } - if (this.skipCheckSuites.has(event.check_suite_id)) { - return - } - const pullRequests = await this.pullRequestCoordinator.getAllPullRequests( repository ) @@ -273,6 +269,16 @@ export class NotificationsStore { return } + // Make sure we haven't shown a notification for the check runs of this + // check suite already. + const checkSuiteCheckRunIds = checks.flatMap(check => + check.checkSuiteId === event.check_suite_id ? check.id : [] + ) + + if (checkSuiteCheckRunIds.some(id => this.skipCheckRuns.has(id))) { + return + } + const numberOfFailedChecks = checks.filter( check => check.conclusion === APICheckConclusion.Failure ).length @@ -284,11 +290,11 @@ export class NotificationsStore { return } - // Ignore any remaining notification for check suites that started along + // Ignore any remaining notification for check runs that started along // with this one. for (const check of checks) { if (check.checkSuiteId !== null) { - this.skipCheckSuites.add(check.checkSuiteId) + this.skipCheckRuns.add(check.id) } } @@ -387,7 +393,7 @@ export class NotificationsStore { private resetCache() { this.cachedCommits.clear() this.skipCommitShas.clear() - this.skipCheckSuites.clear() + this.skipCheckRuns.clear() } /** From 5d4fa4f2bbbecbdeb4c60a61bde3ee7fb39b567b Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 16 Nov 2022 13:46:51 +0100 Subject: [PATCH 197/262] No need to check the check suite id anymore --- app/src/lib/stores/notifications-store.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index 419f8fb196..dc9c96b4eb 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -293,9 +293,7 @@ export class NotificationsStore { // Ignore any remaining notification for check runs that started along // with this one. for (const check of checks) { - if (check.checkSuiteId !== null) { - this.skipCheckRuns.add(check.id) - } + this.skipCheckRuns.add(check.id) } const pluralChecks = From b86157f97ff901aec9c0e99605f369660d63ea7b Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 16 Nov 2022 15:16:08 +0100 Subject: [PATCH 198/262] Check for every check run id Co-Authored-By: tidy-dev <75402236+tidy-dev@users.noreply.github.com> --- app/src/lib/stores/notifications-store.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/lib/stores/notifications-store.ts b/app/src/lib/stores/notifications-store.ts index dc9c96b4eb..b5c61ab29d 100644 --- a/app/src/lib/stores/notifications-store.ts +++ b/app/src/lib/stores/notifications-store.ts @@ -271,11 +271,13 @@ export class NotificationsStore { // 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.some(id => this.skipCheckRuns.has(id))) { + if (checkSuiteCheckRunIds.every(id => this.skipCheckRuns.has(id))) { return } From 51a5d7cd4f8bc795d1840b4af95ec017945edb9f Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Tue, 15 Nov 2022 17:48:16 +0100 Subject: [PATCH 199/262] Retain list item focus while scrolling Moves focus away from unmounted list items to the grid such that keyboard navigation still works after scrolling Co-Authored-By: Mark Hicken <849930+markhicken@users.noreply.github.com> --- app/src/ui/lib/list/list-row.tsx | 30 ++++++++++++++- app/src/ui/lib/list/list.tsx | 64 +++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/app/src/ui/lib/list/list-row.tsx b/app/src/ui/lib/list/list-row.tsx index dc0c762b56..d1e219913b 100644 --- a/app/src/ui/lib/list/list-row.tsx +++ b/app/src/ui/lib/list/list-row.tsx @@ -24,7 +24,7 @@ interface IListRowProps { readonly selected?: boolean /** callback to fire when the DOM element is created */ - readonly onRef?: (element: HTMLDivElement | null) => void + readonly onRowRef?: (index: number, element: HTMLDivElement | null) => void /** callback to fire when the row receives a mouseover event */ readonly onRowMouseOver: (index: number, e: React.MouseEvent) => void @@ -41,6 +41,18 @@ interface IListRowProps { /** callback to fire when the row receives a keyboard event */ readonly onRowKeyDown: (index: number, e: React.KeyboardEvent) => void + /** called when the row (or any of its descendants) receives focus */ + readonly onRowFocus?: ( + index: number, + e: React.FocusEvent + ) => void + + /** called when the row (and all of its descendants) loses focus */ + readonly onRowBlur?: ( + index: number, + e: React.FocusEvent + ) => void + /** * Whether or not this list row is going to be selectable either through * keyboard navigation, pointer clicks, or both. This is used to determine @@ -53,6 +65,10 @@ interface IListRowProps { } export class ListRow extends React.Component { + private onRef = (elem: HTMLDivElement | null) => { + this.props.onRowRef?.(this.props.rowIndex, elem) + } + private onRowMouseOver = (e: React.MouseEvent) => { this.props.onRowMouseOver(this.props.rowIndex, e) } @@ -73,6 +89,14 @@ export class ListRow extends React.Component { this.props.onRowKeyDown(this.props.rowIndex, e) } + private onFocus = (e: React.FocusEvent) => { + this.props.onRowFocus?.(this.props.rowIndex, e) + } + + private onBlur = (e: React.FocusEvent) => { + this.props.onRowBlur?.(this.props.rowIndex, e) + } + public render() { const selected = this.props.selected const className = classNames( @@ -102,13 +126,15 @@ export class ListRow extends React.Component { role={role} className={className} tabIndex={this.props.tabIndex} - ref={this.props.onRef} + ref={this.onRef} onMouseOver={this.onRowMouseOver} onMouseDown={this.onRowMouseDown} onMouseUp={this.onRowMouseUp} onClick={this.onRowClick} onKeyDown={this.onRowKeyDown} style={style} + onFocus={this.onFocus} + onBlur={this.onBlur} > {this.props.children}
diff --git a/app/src/ui/lib/list/list.tsx b/app/src/ui/lib/list/list.tsx index c3aa3bdba2..bed01393b2 100644 --- a/app/src/ui/lib/list/list.tsx +++ b/app/src/ui/lib/list/list.tsx @@ -269,6 +269,8 @@ export class List extends React.Component { private fakeScroll: HTMLDivElement | null = null private focusRow = -1 + private readonly rowRefs = new Map() + /** * The style prop for our child Grid. We keep this here in order * to not create a new object on each render and thus forcing @@ -567,6 +569,15 @@ export class List extends React.Component { } } + 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) => { this.props.selectedRows.forEach(row => { if (!this.props.onRowClick) { @@ -586,6 +597,16 @@ export class List extends React.Component { }) } + private onRowFocus = (index: number, e: React.FocusEvent) => { + this.focusRow = index + } + + private onRowBlur = (index: number, e: React.FocusEvent) => { + if (this.focusRow === index) { + this.focusRow = -1 + } + } + private onRowMouseOver = (row: number, event: React.MouseEvent) => { if (this.props.selectOnHover && this.canSelectRow(row)) { if (!this.props.selectedRows.includes(row)) { @@ -595,7 +616,7 @@ export class List extends React.Component { // more importantly `scrollRowToVisible` automatically manages focus so // using it here allows us to piggy-back on its focus-preserving magic // even though we could theoretically live without scrolling - this.scrollRowToVisible(row) + this.scrollRowToVisible(row, this.props.focusOnHover !== false) } } } @@ -717,10 +738,14 @@ export class List extends React.Component { this.scrollRowToVisible(row) } - private scrollRowToVisible(row: number) { + private scrollRowToVisible(row: number, moveFocus = true) { if (this.grid !== null) { this.grid.scrollToCell({ rowIndex: row, columnIndex: 0 }) - this.focusRow = row + + if (moveFocus) { + this.focusRow = row + this.rowRefs.get(row)?.focus({ preventScroll: true }) + } } } @@ -801,12 +826,27 @@ export class List extends React.Component { } } - private onFocusedItemRef = (element: HTMLDivElement | null) => { - if (this.props.focusOnHover !== false && element !== null) { - element.focus() + private onRowRef = (rowIndex: number, element: HTMLDivElement | null) => { + if (element === null) { + this.rowRefs.delete(rowIndex) + } else { + this.rowRefs.set(rowIndex, element) } - this.focusRow = -1 + if (rowIndex === this.focusRow) { + // The currently focused row is going being unmounted so we'll move focus + // programmatically to the grid so that keyboard navigation still works + if (element === null) { + const grid = ReactDOM.findDOMNode(this.grid) + if (grid instanceof HTMLElement) { + grid.focus({ preventScroll: true }) + } + } else { + // A previously focused row is being mounted again, we'll move focus + // back to it + element.focus({ preventScroll: true }) + } + } } private getCustomRowClassNames = (rowIndex: number) => { @@ -833,17 +873,12 @@ export class List extends React.Component { const selected = this.props.selectedRows.indexOf(rowIndex) !== -1 const customClasses = this.getCustomRowClassNames(rowIndex) - const focused = rowIndex === this.focusRow - // An unselectable row shouldn't be focusable let tabIndex: number | undefined = undefined if (selectable) { tabIndex = selected && this.props.selectedRows[0] === rowIndex ? 0 : -1 } - // We only need to keep a reference to the focused element - const ref = focused ? this.onFocusedItemRef : undefined - const row = this.props.rowRenderer(rowIndex) const element = @@ -867,7 +902,7 @@ export class List extends React.Component { { onRowMouseDown={this.onRowMouseDown} onRowMouseUp={this.onRowMouseUp} onRowMouseOver={this.onRowMouseOver} + onRowFocus={this.onRowFocus} + onRowBlur={this.onRowBlur} style={params.style} tabIndex={tabIndex} children={element} @@ -975,6 +1012,7 @@ export class List extends React.Component { Date: Fri, 18 Nov 2022 16:44:07 +0100 Subject: [PATCH 200/262] Update README screenshot with light and dark versions --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a2a683452..f4d7975147 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,17 @@ GitHub app. It is written in [TypeScript](https://www.typescriptlang.org) and uses [React](https://reactjs.org/). -![GitHub Desktop screenshot - Windows](https://cloud.githubusercontent.com/assets/359239/26094502/a1f56d02-3a5d-11e7-8799-23c7ba5e5106.png) + + + A screenshot of the GitHub Desktop application showing changes being viewed and committed with two attributed co-authors + ## Where can I get it? From bc8bbd5917f23d48768e28481a7d7e4f0c3aa43a Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 18 Nov 2022 16:54:42 +0100 Subject: [PATCH 201/262] Render ahead/behind tooltip using custom Tooltip --- app/src/ui/repositories-list/repository-list-item.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/ui/repositories-list/repository-list-item.tsx b/app/src/ui/repositories-list/repository-list-item.tsx index 27408b4c45..b172d46b6d 100644 --- a/app/src/ui/repositories-list/repository-list-item.tsx +++ b/app/src/ui/repositories-list/repository-list-item.tsx @@ -193,10 +193,14 @@ const renderAheadBehindIndicator = (aheadBehind: IAheadBehind) => { 'its tracked branch.' return ( -
+ {ahead > 0 && } {behind > 0 && } -
+ ) } From f1e74e526afdb656e9a7156c0f2341e6eb0fa94e Mon Sep 17 00:00:00 2001 From: Shivareddy-Aluri Date: Tue, 22 Nov 2022 09:02:17 +0530 Subject: [PATCH 202/262] linters fix --- app/src/ui/history/commit-list-item.tsx | 8 +++++--- app/src/ui/history/commit-summary.tsx | 5 ++++- app/styles/ui/history/_commit-summary.scss | 1 - 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/ui/history/commit-list-item.tsx b/app/src/ui/history/commit-list-item.tsx index fba4744e59..d35a725b02 100644 --- a/app/src/ui/history/commit-list-item.tsx +++ b/app/src/ui/history/commit-list-item.tsx @@ -345,8 +345,10 @@ export class CommitListItem extends React.PureComponent< deleteTagsMenuItem ) } - const darwinTagsLabel = this.props.commit.tags.length > 1 ? 'Copy Tags' : 'Copy Tag'; - const windowTagsLabel = this.props.commit.tags.length > 1 ? 'Copy tags' : 'Copy tag'; + const darwinTagsLabel = + this.props.commit.tags.length > 1 ? 'Copy Tags' : 'Copy Tag' + const windowTagsLabel = + this.props.commit.tags.length > 1 ? 'Copy tags' : 'Copy tag' items.push( { label: __DARWIN__ ? 'Cherry-pick Commit…' : 'Cherry-pick commit…', @@ -361,7 +363,7 @@ export class CommitListItem extends React.PureComponent< { label: __DARWIN__ ? darwinTagsLabel : windowTagsLabel, action: this.onCopyTags, - enabled: this.props.commit.tags.length > 0 + enabled: this.props.commit.tags.length > 0, }, { label: viewOnGitHubLabel, diff --git a/app/src/ui/history/commit-summary.tsx b/app/src/ui/history/commit-summary.tsx index 6437d4b0c9..904f195617 100644 --- a/app/src/ui/history/commit-summary.tsx +++ b/app/src/ui/history/commit-summary.tsx @@ -430,7 +430,10 @@ export class CommitSummary extends React.Component< aria-label="SHA" > - + ) } diff --git a/app/styles/ui/history/_commit-summary.scss b/app/styles/ui/history/_commit-summary.scss index 9e7990d3c6..5518f52ad2 100644 --- a/app/styles/ui/history/_commit-summary.scss +++ b/app/styles/ui/history/_commit-summary.scss @@ -84,7 +84,6 @@ // for a class name one finds in tsx. We might want to consider not allowing // this in the future but that's for... the future. .commit-summary { - &-title, &-meta { padding: var(--spacing); From 88a358b4b5d520a415e1d31250714c7528c52e08 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 22 Nov 2022 10:31:36 -0500 Subject: [PATCH 203/262] Add a method to get all the popups in the stack --- app/src/lib/popup-manager.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index ad74aabab7..20abd04c20 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -47,8 +47,19 @@ export class PopupManager { * otherwise returns the first non-error type popup. */ public get currentPopup(): Popup | null { + return this.allPopups.at(-1) ?? null + } + + /** + * Returns all the popups in the stack. If there are error popups, it returns + * them on the top of the stack (the end of the array -> last on, last off). + */ + public get allPopups(): ReadonlyArray { const errorPopups = this.getPopupsOfType(PopupType.Error) - return errorPopups.at(-1) ?? this.popupStack.at(-1) ?? null + const nonErrorPopups = this.popupStack.filter( + p => p.type !== PopupType.Error + ) + return [...nonErrorPopups, ...errorPopups] } /** From 9a990d19b9999fe0b8a346da4951a6e08fb5e917 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 22 Nov 2022 10:32:58 -0500 Subject: [PATCH 204/262] Add allPopups to the app state --- app/src/lib/app-state.ts | 1 + app/src/lib/stores/app-store.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index 69c32a28c9..37ce893019 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -119,6 +119,7 @@ export interface IAppState { readonly showWelcomeFlow: boolean readonly focusCommitMessage: boolean readonly currentPopup: Popup | null + readonly allPopups: ReadonlyArray readonly currentFoldout: Foldout | null readonly currentBanner: Banner | null diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 728af6d5a5..db458adb7d 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -930,6 +930,7 @@ export class AppStore extends TypedBaseStore { selectedState: this.getSelectedState(), signInState: this.signInStore.getState(), currentPopup: this.popupManager.currentPopup, + allPopups: this.popupManager.allPopups, currentFoldout: this.currentFoldout, errorCount: this.popupManager.getPopupsOfType(PopupType.Error).length, showWelcomeFlow: this.showWelcomeFlow, From 431b2d06539100fd63852dc3f4ef9143313afd1f Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 22 Nov 2022 10:41:33 -0500 Subject: [PATCH 205/262] Render all popups in the stack --- app/src/ui/app.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 03c596b3e6..9883f539a4 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -1397,13 +1397,23 @@ export class App extends React.Component { private onUpdateAvailableDismissed = () => this.props.dispatcher.setUpdateBannerVisibility(false) - private currentPopupContent(): JSX.Element | null { - const popup = this.state.currentPopup + private allPopupContent(): JSX.Element | null { + const { allPopups } = this.state - if (!popup) { + if (allPopups.length === 0) { return null } + return ( + <> + {allPopups.map(popup => { + return
{this.popupContent(popup)}
+ })} + + ) + } + + private popupContent(popup: Popup): JSX.Element | null { if (popup.id === undefined) { // Should not be possible... but if it does we want to know about it. sendNonFatalException( @@ -2461,8 +2471,8 @@ export class App extends React.Component { this.props.dispatcher.showPopup({ type: PopupType.TermsAndConditions }) } - private renderPopup() { - const popupContent = this.currentPopupContent() + private renderPopups() { + const popupContent = this.allPopupContent() return ( @@ -2550,7 +2560,7 @@ export class App extends React.Component { {this.renderToolbar()} {this.renderBanner()} {this.renderRepository()} - {this.renderPopup()} + {this.renderPopups()} {this.renderDragElement()}
) From 4d34bedd5c1583f119e4b44badd070ca0a075e1a Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 22 Nov 2022 11:12:17 -0500 Subject: [PATCH 206/262] Add Dialog Stack Context to the dialog component --- app/src/ui/dialog/dialog.tsx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index 3ac49972e0..4c89c68713 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -4,6 +4,31 @@ import { DialogHeader } from './header' import { createUniqueId, releaseUniqueId } from '../lib/id-pool' import { getTitleBarHeight } from '../window/title-bar' +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({ + isTopMost: false, +}) + /** * The time (in milliseconds) from when the dialog is mounted * until it can be dismissed. See the isAppearing property in @@ -138,6 +163,9 @@ interface IDialogState { * out of the dialog without first dismissing it. */ export class Dialog extends React.Component { + public static contextType = DialogStackContext + public declare context: React.ContextType + private dialogElement: HTMLDialogElement | null = null private dismissGraceTimeoutId?: number From 03dea5741b8ee2f1b11bb792ea0ee5f3d07d778e Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 22 Nov 2022 11:28:06 -0500 Subject: [PATCH 207/262] Implement DialogStackContext for showing/closing modal --- app/src/lib/globals.d.ts | 2 ++ app/src/ui/app.tsx | 13 ++++++++++++- app/src/ui/dialog/dialog.tsx | 18 +++++++++++++++++- app/styles/ui/_dialog.scss | 4 ++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/app/src/lib/globals.d.ts b/app/src/lib/globals.d.ts index ea4b3c0416..e1d4aa4a04 100644 --- a/app/src/lib/globals.d.ts +++ b/app/src/lib/globals.d.ts @@ -162,6 +162,8 @@ interface Window { interface HTMLDialogElement { showModal: () => void + close: (returnValue?: string | undefined) => void + open: boolean } /** * Obtain the number of elements of a tuple type diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 9883f539a4..89602847ad 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -165,6 +165,7 @@ 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 { DialogStackContext, IDialogStackContext } from './dialog' const MinuteInMilliseconds = 1000 * 60 const HourInMilliseconds = MinuteInMilliseconds * 60 @@ -1407,7 +1408,17 @@ export class App extends React.Component { return ( <> {allPopups.map(popup => { - return
{this.popupContent(popup)}
+ const dialogStackContext: IDialogStackContext = { + isTopMost: this.state.currentPopup?.id === popup.id, + } + return ( + + {this.popupContent(popup)} + + ) })} ) diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index 4c89c68713..766a42fb0b 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -281,7 +281,7 @@ export class Dialog extends React.Component { return } - this.dialogElement.showModal() + this.updateDialogAvailability() // Provide an event that components can subscribe to in order to perform // tasks such as re-layout after the dialog is visible @@ -303,6 +303,20 @@ export class Dialog extends React.Component { window.addEventListener('resize', this.scheduleResizeEvent) } + private updateDialogAvailability = () => { + if (this.dialogElement == null) { + return + } + + if (this.context.isTopMost && !this.dialogElement.open) { + this.dialogElement.showModal() + } + + if (!this.context.isTopMost && this.dialogElement.open) { + this.dialogElement.close() + } + } + /** * Attempts to move keyboard focus to the first _suitable_ child of the * dialog. @@ -470,6 +484,8 @@ export class Dialog extends React.Component { if (!this.props.title && this.state.titleId) { this.updateTitleId() } + + this.updateDialogAvailability() } private onDialogCancel = (e: Event | React.SyntheticEvent) => { diff --git a/app/styles/ui/_dialog.scss b/app/styles/ui/_dialog.scss index 87111bfbe7..2bc163b522 100644 --- a/app/styles/ui/_dialog.scss +++ b/app/styles/ui/_dialog.scss @@ -129,6 +129,10 @@ dialog { opacity: 1; } + &:not([open]) { + display: none !important; + } + // The dialog embeds a fieldset as the first child of the form element // in order to be able to disable all form elements and buttons in one // swoop. This resets all styles for that fieldset. From 3a41ad5ede91d694e43f42d00196e0fdce52659d Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 22 Nov 2022 12:14:48 -0500 Subject: [PATCH 208/262] memoizeOne the mounting/unmounting of a dialog that happens when it is the top most --- app/src/ui/dialog/dialog.tsx | 55 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index 766a42fb0b..57d6f8d85f 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames' import { DialogHeader } from './header' import { createUniqueId, releaseUniqueId } from '../lib/id-pool' import { getTitleBarHeight } from '../window/title-bar' +import memoizeOne from 'memoize-one' export interface IDialogStackContext { /** Whether or not this dialog is the top most one in the stack to be @@ -172,6 +173,20 @@ export class Dialog extends React.Component { private disableClickDismissalTimeoutId: number | null = null private disableClickDismissal = false + private updateDialogAvailability = memoizeOne((isTopMost: boolean) => { + if (this.dialogElement == null) { + return + } + + if (isTopMost && !this.dialogElement.open) { + this.onDialogIsTopMost(this.dialogElement) + } + + if (!isTopMost && this.dialogElement.open) { + this.onDialogIsNotTopMost(this.dialogElement) + } + }) + /** * Resize observer used for tracking width changes and * refreshing the internal codemirror instance when @@ -277,15 +292,15 @@ export class Dialog extends React.Component { } public componentDidMount() { - if (!this.dialogElement) { - return - } + this.updateDialogAvailability(this.context.isTopMost) + } - this.updateDialogAvailability() + private onDialogIsTopMost(dialogElement: HTMLDialogElement) { + dialogElement.showModal() // Provide an event that components can subscribe to in order to perform // tasks such as re-layout after the dialog is visible - this.dialogElement.dispatchEvent( + dialogElement.dispatchEvent( new CustomEvent('dialog-show', { bubbles: true, cancelable: false, @@ -299,22 +314,20 @@ export class Dialog extends React.Component { window.addEventListener('focus', this.onWindowFocus) - this.resizeObserver.observe(this.dialogElement) + this.resizeObserver.observe(dialogElement) window.addEventListener('resize', this.scheduleResizeEvent) } - private updateDialogAvailability = () => { - if (this.dialogElement == null) { - return - } + private onDialogIsNotTopMost(dialogElement: HTMLDialogElement) { + dialogElement.close() - if (this.context.isTopMost && !this.dialogElement.open) { - this.dialogElement.showModal() - } + this.clearDismissGraceTimeout() - if (!this.context.isTopMost && this.dialogElement.open) { - this.dialogElement.close() - } + window.removeEventListener('focus', this.onWindowFocus) + document.removeEventListener('mouseup', this.onDocumentMouseUp) + + this.resizeObserver.disconnect() + window.removeEventListener('resize', this.scheduleResizeEvent) } /** @@ -467,17 +480,9 @@ export class Dialog extends React.Component { } public componentWillUnmount() { - this.clearDismissGraceTimeout() - if (this.state.titleId) { releaseUniqueId(this.state.titleId) } - - window.removeEventListener('focus', this.onWindowFocus) - document.removeEventListener('mouseup', this.onDocumentMouseUp) - - this.resizeObserver.disconnect() - window.removeEventListener('resize', this.scheduleResizeEvent) } public componentDidUpdate() { @@ -485,7 +490,7 @@ export class Dialog extends React.Component { this.updateTitleId() } - this.updateDialogAvailability() + this.updateDialogAvailability(this.context.isTopMost) } private onDialogCancel = (e: Event | React.SyntheticEvent) => { From 6001d01cc1fb213061ddd98a19f374444e5f427b Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 22 Nov 2022 12:34:57 -0500 Subject: [PATCH 209/262] Add IsTopMost check to other dialogs with event listeners --- app/src/ui/about/about.tsx | 29 ++++++++++++++-- .../ui/add-repository/create-repository.tsx | 34 +++++++++++++++++-- .../ui/clone-repository/clone-repository.tsx | 32 +++++++++++++++-- app/src/ui/dialog/dialog.tsx | 11 +++--- 4 files changed, 95 insertions(+), 11 deletions(-) diff --git a/app/src/ui/about/about.tsx b/app/src/ui/about/about.tsx index b7d8854f2e..16e16c6232 100644 --- a/app/src/ui/about/about.tsx +++ b/app/src/ui/about/about.tsx @@ -7,6 +7,7 @@ import { DialogError, DialogContent, DefaultDialogFooter, + DialogStackContext, } from '../dialog' import { LinkButton } from '../lib/link-button' import { updateStore, IUpdateState, UpdateStatus } from '../lib/update-store' @@ -16,6 +17,7 @@ import { RelativeTime } from '../relative-time' import { assertNever } from '../../lib/fatal-error' import { ReleaseNotesUri } from '../lib/releases' import { encodePathAsUrl } from '../../lib/path' +import memoizeOne from 'memoize-one' const logoPath = __DARWIN__ ? 'static/logo-64x64@2x.png' @@ -66,6 +68,17 @@ interface IAboutState { * running application such as name and version. */ export class About extends React.Component { + public static contextType = DialogStackContext + public declare context: React.ContextType + + private checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { + if (isTopMost) { + this.onDialogIsTopMost() + } else { + this.onDialogIsNotTopMost() + } + }) + private updateStoreEventHandle: Disposable | null = null public constructor(props: IAboutProps) { @@ -81,13 +94,16 @@ export class About extends React.Component { this.setState({ updateState }) } + public componentDidUpdate(): void { + this.checkWhetherDialogIsTopMost(this.context.isTopMost) + } + public componentDidMount() { this.updateStoreEventHandle = updateStore.onDidChange( this.onUpdateStateChanged ) this.setState({ updateState: updateStore.state }) - window.addEventListener('keydown', this.onKeyDown) - window.addEventListener('keyup', this.onKeyUp) + this.checkWhetherDialogIsTopMost(this.context.isTopMost) } public componentWillUnmount() { @@ -95,6 +111,15 @@ export class About extends React.Component { this.updateStoreEventHandle.dispose() this.updateStoreEventHandle = null } + this.onDialogIsNotTopMost() + } + + private onDialogIsTopMost() { + window.addEventListener('keydown', this.onKeyDown) + window.addEventListener('keyup', this.onKeyUp) + } + + private onDialogIsNotTopMost() { window.removeEventListener('keydown', this.onKeyDown) window.removeEventListener('keyup', this.onKeyUp) } diff --git a/app/src/ui/add-repository/create-repository.tsx b/app/src/ui/add-repository/create-repository.tsx index 96165be107..d2566ee954 100644 --- a/app/src/ui/add-repository/create-repository.tsx +++ b/app/src/ui/add-repository/create-repository.tsx @@ -22,7 +22,13 @@ import { getGitIgnoreNames, writeGitIgnore } from './gitignores' import { ILicense, getLicenses, writeLicense } from './licenses' import { writeGitAttributes } from './git-attributes' import { getDefaultDir, setDefaultDir } from '../lib/default-dir' -import { Dialog, DialogContent, DialogFooter, DialogError } from '../dialog' +import { + Dialog, + DialogContent, + DialogFooter, + DialogError, + DialogStackContext, +} from '../dialog' import { Octicon } from '../octicons' import * as OcticonSymbol from '../octicons/octicons.generated' import { LinkButton } from '../lib/link-button' @@ -36,6 +42,7 @@ import { mkdir } from 'fs/promises' import { directoryExists } from '../../lib/directory-exists' import { FoldoutType } from '../../lib/app-state' import { join } from 'path' +import memoizeOne from 'memoize-one' /** The sentinel value used to indicate no gitignore should be used. */ const NoGitIgnoreValue = 'None' @@ -115,6 +122,17 @@ export class CreateRepository extends React.Component< ICreateRepositoryProps, ICreateRepositoryState > { + public static contextType = DialogStackContext + public declare context: React.ContextType + + private checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { + if (isTopMost) { + this.onDialogIsTopMost() + } else { + this.onDialogIsNotTopMost() + } + }) + public constructor(props: ICreateRepositoryProps) { super(props) @@ -144,8 +162,12 @@ export class CreateRepository extends React.Component< } } + public componentDidUpdate(): void { + this.checkWhetherDialogIsTopMost(this.context.isTopMost) + } + public async componentDidMount() { - window.addEventListener('focus', this.onWindowFocus) + this.checkWhetherDialogIsTopMost(this.context.isTopMost) const gitIgnoreNames = await getGitIgnoreNames() const licenses = await getLicenses() @@ -159,6 +181,14 @@ export class CreateRepository extends React.Component< } public componentWillUnmount() { + this.onDialogIsNotTopMost() + } + + private onDialogIsTopMost() { + window.addEventListener('focus', this.onWindowFocus) + } + + private onDialogIsNotTopMost() { window.removeEventListener('focus', this.onWindowFocus) } diff --git a/app/src/ui/clone-repository/clone-repository.tsx b/app/src/ui/clone-repository/clone-repository.tsx index cfca9408fc..612d73a5a2 100644 --- a/app/src/ui/clone-repository/clone-repository.tsx +++ b/app/src/ui/clone-repository/clone-repository.tsx @@ -11,7 +11,13 @@ import { } from '../../lib/remote-parsing' import { findAccountForRemoteURL } from '../../lib/find-account' import { API, IAPIRepository, IAPIRepositoryCloneInfo } from '../../lib/api' -import { Dialog, DialogError, DialogFooter, DialogContent } from '../dialog' +import { + Dialog, + DialogError, + DialogFooter, + DialogContent, + DialogStackContext, +} from '../dialog' import { TabBar } from '../tab-bar' import { CloneRepositoryTab } from '../../models/clone-repository-tab' import { CloneGenericRepository } from './clone-generic-repository' @@ -24,6 +30,7 @@ import { ClickSource } from '../lib/list' import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' import { showOpenDialog, showSaveDialog } from '../main-process-proxy' import { readdir } from 'fs/promises' +import memoizeOne from 'memoize-one' interface ICloneRepositoryProps { readonly dispatcher: Dispatcher @@ -148,6 +155,17 @@ export class CloneRepository extends React.Component< ICloneRepositoryProps, ICloneRepositoryState > { + public static contextType = DialogStackContext + public declare context: React.ContextType + + private checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { + if (isTopMost) { + this.onDialogIsTopMost() + } else { + this.onDialogIsNotTopMost() + } + }) + public constructor(props: ICloneRepositoryProps) { super(props) @@ -192,6 +210,8 @@ export class CloneRepository extends React.Component< if (prevProps.initialURL !== this.props.initialURL) { this.updateUrl(this.props.initialURL || '') } + + this.checkWhetherDialogIsTopMost(this.context.isTopMost) } public componentDidMount() { @@ -200,7 +220,7 @@ export class CloneRepository extends React.Component< this.updateUrl(initialURL) } - window.addEventListener('focus', this.onWindowFocus) + this.checkWhetherDialogIsTopMost(this.context.isTopMost) } private initializePath = async () => { @@ -225,6 +245,14 @@ export class CloneRepository extends React.Component< } public componentWillUnmount() { + this.onDialogIsNotTopMost() + } + + private onDialogIsTopMost() { + window.addEventListener('focus', this.onWindowFocus) + } + + private onDialogIsNotTopMost() { window.removeEventListener('focus', this.onWindowFocus) } diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index 57d6f8d85f..54b5f7f626 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -173,7 +173,7 @@ export class Dialog extends React.Component { private disableClickDismissalTimeoutId: number | null = null private disableClickDismissal = false - private updateDialogAvailability = memoizeOne((isTopMost: boolean) => { + private checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { if (this.dialogElement == null) { return } @@ -292,7 +292,7 @@ export class Dialog extends React.Component { } public componentDidMount() { - this.updateDialogAvailability(this.context.isTopMost) + this.checkWhetherDialogIsTopMost(this.context.isTopMost) } private onDialogIsTopMost(dialogElement: HTMLDialogElement) { @@ -318,8 +318,8 @@ export class Dialog extends React.Component { window.addEventListener('resize', this.scheduleResizeEvent) } - private onDialogIsNotTopMost(dialogElement: HTMLDialogElement) { - dialogElement.close() + private onDialogIsNotTopMost(dialogElement?: HTMLDialogElement) { + dialogElement?.close() this.clearDismissGraceTimeout() @@ -483,6 +483,7 @@ export class Dialog extends React.Component { if (this.state.titleId) { releaseUniqueId(this.state.titleId) } + this.onDialogIsNotTopMost() } public componentDidUpdate() { @@ -490,7 +491,7 @@ export class Dialog extends React.Component { this.updateTitleId() } - this.updateDialogAvailability(this.context.isTopMost) + this.checkWhetherDialogIsTopMost(this.context.isTopMost) } private onDialogCancel = (e: Event | React.SyntheticEvent) => { From 7f0393d450c78f6e6ebdcc2c48728476d475941d Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 22 Nov 2022 14:10:11 -0500 Subject: [PATCH 210/262] !important not necessary --- app/styles/ui/_dialog.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/ui/_dialog.scss b/app/styles/ui/_dialog.scss index 2bc163b522..3669ea9fd1 100644 --- a/app/styles/ui/_dialog.scss +++ b/app/styles/ui/_dialog.scss @@ -130,7 +130,7 @@ dialog { } &:not([open]) { - display: none !important; + display: none; } // The dialog embeds a fieldset as the first child of the form element From 6be77aae93e8c39c0eb13132645fca616a573093 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 22 Nov 2022 14:10:37 -0500 Subject: [PATCH 211/262] Feature flag rendering all popups at once --- app/src/ui/app.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 89602847ad..bb013a3efd 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -166,6 +166,7 @@ import { createCommitURL } from '../lib/commit-url' import { uuid } from '../lib/uuid' import { InstallingUpdate } from './installing-update/installing-update' import { DialogStackContext, IDialogStackContext } from './dialog' +import { enableStackedPopups } from '../lib/feature-flag' const MinuteInMilliseconds = 1000 * 60 const HourInMilliseconds = MinuteInMilliseconds * 60 @@ -1399,7 +1400,11 @@ export class App extends React.Component { this.props.dispatcher.setUpdateBannerVisibility(false) private allPopupContent(): JSX.Element | null { - const { allPopups } = this.state + let { allPopups } = this.state + + if (!enableStackedPopups() && this.state.currentPopup !== null) { + allPopups = [this.state.currentPopup] + } if (allPopups.length === 0) { return null From 2f35a2db1cd7e73e35f918e7801b5ec53fe63376 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:18:27 +0000 Subject: [PATCH 212/262] Bump peter-evans/create-pull-request from 4.2.0 to 4.2.3 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.2.0 to 4.2.3. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v4.2.0...v4.2.3) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 944398dcb3..c5dfb6fdb1 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -37,7 +37,7 @@ jobs: private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} - name: Create Release Pull Request - uses: peter-evans/create-pull-request@v4.2.0 + uses: peter-evans/create-pull-request@v4.2.3 if: | startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test') with: From cc10f0adf2e67e85dafae9a477fbf687b9f582ab Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 28 Nov 2022 11:10:19 -0500 Subject: [PATCH 213/262] Tidying - Move repeated logic to context consumer class Co-Authored-By: Sergio Padrino <1083228+sergiou87@users.noreply.github.com> --- app/src/ui/about/about.tsx | 31 +++----- .../ui/add-repository/create-repository.tsx | 37 ++-------- app/src/ui/app.tsx | 5 +- .../ui/clone-repository/clone-repository.tsx | 35 ++------- .../dialog/dialog-stack-context-consumer.tsx | 74 +++++++++++++++++++ app/src/ui/dialog/dialog.tsx | 66 ++++++----------- 6 files changed, 123 insertions(+), 125 deletions(-) create mode 100644 app/src/ui/dialog/dialog-stack-context-consumer.tsx diff --git a/app/src/ui/about/about.tsx b/app/src/ui/about/about.tsx index 16e16c6232..481703b1ad 100644 --- a/app/src/ui/about/about.tsx +++ b/app/src/ui/about/about.tsx @@ -7,7 +7,6 @@ import { DialogError, DialogContent, DefaultDialogFooter, - DialogStackContext, } from '../dialog' import { LinkButton } from '../lib/link-button' import { updateStore, IUpdateState, UpdateStatus } from '../lib/update-store' @@ -17,7 +16,7 @@ import { RelativeTime } from '../relative-time' import { assertNever } from '../../lib/fatal-error' import { ReleaseNotesUri } from '../lib/releases' import { encodePathAsUrl } from '../../lib/path' -import memoizeOne from 'memoize-one' +import { DialogStackContextConsumer } from '../dialog/dialog-stack-context-consumer' const logoPath = __DARWIN__ ? 'static/logo-64x64@2x.png' @@ -67,18 +66,10 @@ interface IAboutState { * A dialog that presents information about the * running application such as name and version. */ -export class About extends React.Component { - public static contextType = DialogStackContext - public declare context: React.ContextType - - private checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { - if (isTopMost) { - this.onDialogIsTopMost() - } else { - this.onDialogIsNotTopMost() - } - }) - +export class About extends DialogStackContextConsumer< + IAboutProps, + IAboutState +> { private updateStoreEventHandle: Disposable | null = null public constructor(props: IAboutProps) { @@ -94,16 +85,12 @@ export class About extends React.Component { this.setState({ updateState }) } - public componentDidUpdate(): void { - this.checkWhetherDialogIsTopMost(this.context.isTopMost) - } - public componentDidMount() { this.updateStoreEventHandle = updateStore.onDidChange( this.onUpdateStateChanged ) this.setState({ updateState: updateStore.state }) - this.checkWhetherDialogIsTopMost(this.context.isTopMost) + super.componentDidMount() } public componentWillUnmount() { @@ -111,15 +98,15 @@ export class About extends React.Component { this.updateStoreEventHandle.dispose() this.updateStoreEventHandle = null } - this.onDialogIsNotTopMost() + super.componentWillUnmount() } - private onDialogIsTopMost() { + protected onDialogIsTopMost() { window.addEventListener('keydown', this.onKeyDown) window.addEventListener('keyup', this.onKeyUp) } - private onDialogIsNotTopMost() { + protected onDialogIsNotTopMost() { window.removeEventListener('keydown', this.onKeyDown) window.removeEventListener('keyup', this.onKeyUp) } diff --git a/app/src/ui/add-repository/create-repository.tsx b/app/src/ui/add-repository/create-repository.tsx index d2566ee954..c9ad9ab988 100644 --- a/app/src/ui/add-repository/create-repository.tsx +++ b/app/src/ui/add-repository/create-repository.tsx @@ -22,13 +22,7 @@ import { getGitIgnoreNames, writeGitIgnore } from './gitignores' import { ILicense, getLicenses, writeLicense } from './licenses' import { writeGitAttributes } from './git-attributes' import { getDefaultDir, setDefaultDir } from '../lib/default-dir' -import { - Dialog, - DialogContent, - DialogFooter, - DialogError, - DialogStackContext, -} from '../dialog' +import { Dialog, DialogContent, DialogFooter, DialogError } from '../dialog' import { Octicon } from '../octicons' import * as OcticonSymbol from '../octicons/octicons.generated' import { LinkButton } from '../lib/link-button' @@ -42,7 +36,7 @@ import { mkdir } from 'fs/promises' import { directoryExists } from '../../lib/directory-exists' import { FoldoutType } from '../../lib/app-state' import { join } from 'path' -import memoizeOne from 'memoize-one' +import { DialogStackContextConsumer } from '../dialog/dialog-stack-context-consumer' /** The sentinel value used to indicate no gitignore should be used. */ const NoGitIgnoreValue = 'None' @@ -118,21 +112,10 @@ interface ICreateRepositoryState { } /** The Create New Repository component. */ -export class CreateRepository extends React.Component< +export class CreateRepository extends DialogStackContextConsumer< ICreateRepositoryProps, ICreateRepositoryState > { - public static contextType = DialogStackContext - public declare context: React.ContextType - - private checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { - if (isTopMost) { - this.onDialogIsTopMost() - } else { - this.onDialogIsNotTopMost() - } - }) - public constructor(props: ICreateRepositoryProps) { super(props) @@ -162,12 +145,8 @@ export class CreateRepository extends React.Component< } } - public componentDidUpdate(): void { - this.checkWhetherDialogIsTopMost(this.context.isTopMost) - } - public async componentDidMount() { - this.checkWhetherDialogIsTopMost(this.context.isTopMost) + super.componentDidMount() const gitIgnoreNames = await getGitIgnoreNames() const licenses = await getLicenses() @@ -180,15 +159,11 @@ export class CreateRepository extends React.Component< this.updateReadMeExists(path, this.state.name) } - public componentWillUnmount() { - this.onDialogIsNotTopMost() - } - - private onDialogIsTopMost() { + protected onDialogIsTopMost() { window.addEventListener('focus', this.onWindowFocus) } - private onDialogIsNotTopMost() { + protected onDialogIsNotTopMost() { window.removeEventListener('focus', this.onWindowFocus) } diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index bb013a3efd..e6a6464906 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -165,7 +165,10 @@ 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 { DialogStackContext, IDialogStackContext } from './dialog' +import { + DialogStackContext, + IDialogStackContext, +} from './dialog/dialog-stack-context-consumer' import { enableStackedPopups } from '../lib/feature-flag' const MinuteInMilliseconds = 1000 * 60 diff --git a/app/src/ui/clone-repository/clone-repository.tsx b/app/src/ui/clone-repository/clone-repository.tsx index 612d73a5a2..34a1c50abb 100644 --- a/app/src/ui/clone-repository/clone-repository.tsx +++ b/app/src/ui/clone-repository/clone-repository.tsx @@ -11,13 +11,7 @@ import { } from '../../lib/remote-parsing' import { findAccountForRemoteURL } from '../../lib/find-account' import { API, IAPIRepository, IAPIRepositoryCloneInfo } from '../../lib/api' -import { - Dialog, - DialogError, - DialogFooter, - DialogContent, - DialogStackContext, -} from '../dialog' +import { Dialog, DialogError, DialogFooter, DialogContent } from '../dialog' import { TabBar } from '../tab-bar' import { CloneRepositoryTab } from '../../models/clone-repository-tab' import { CloneGenericRepository } from './clone-generic-repository' @@ -30,7 +24,7 @@ import { ClickSource } from '../lib/list' import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' import { showOpenDialog, showSaveDialog } from '../main-process-proxy' import { readdir } from 'fs/promises' -import memoizeOne from 'memoize-one' +import { DialogStackContextConsumer } from '../dialog/dialog-stack-context-consumer' interface ICloneRepositoryProps { readonly dispatcher: Dispatcher @@ -151,21 +145,10 @@ interface IGitHubTabState extends IBaseTabState { } /** The component for cloning a repository. */ -export class CloneRepository extends React.Component< +export class CloneRepository extends DialogStackContextConsumer< ICloneRepositoryProps, ICloneRepositoryState > { - public static contextType = DialogStackContext - public declare context: React.ContextType - - private checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { - if (isTopMost) { - this.onDialogIsTopMost() - } else { - this.onDialogIsNotTopMost() - } - }) - public constructor(props: ICloneRepositoryProps) { super(props) @@ -211,7 +194,7 @@ export class CloneRepository extends React.Component< this.updateUrl(this.props.initialURL || '') } - this.checkWhetherDialogIsTopMost(this.context.isTopMost) + super.componentDidUpdate() } public componentDidMount() { @@ -220,7 +203,7 @@ export class CloneRepository extends React.Component< this.updateUrl(initialURL) } - this.checkWhetherDialogIsTopMost(this.context.isTopMost) + super.componentDidMount() } private initializePath = async () => { @@ -244,15 +227,11 @@ export class CloneRepository extends React.Component< this.updateUrl(selectedTabState.url) } - public componentWillUnmount() { - this.onDialogIsNotTopMost() - } - - private onDialogIsTopMost() { + protected onDialogIsTopMost() { window.addEventListener('focus', this.onWindowFocus) } - private onDialogIsNotTopMost() { + protected onDialogIsNotTopMost() { window.removeEventListener('focus', this.onWindowFocus) } diff --git a/app/src/ui/dialog/dialog-stack-context-consumer.tsx b/app/src/ui/dialog/dialog-stack-context-consumer.tsx new file mode 100644 index 0000000000..76ebf3fd0a --- /dev/null +++ b/app/src/ui/dialog/dialog-stack-context-consumer.tsx @@ -0,0 +1,74 @@ +import memoizeOne from 'memoize-one' +import * as React from 'react' + +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({ + isTopMost: false, +}) + +/** + * A base component for any dialogs that consume the dialog stack context. + * + * This houses logic to respond to when the `isTopMost` changes on the + * `DialogStackContext` by providing two abstract methods of `onDialogIsTopMost` + * and `onDialogIsNotTopMost` and implementing a `checkWhetherDialogIsTopMost + * method that called via the components implementations of React component + * lifecycle methods. + */ +export abstract class DialogStackContextConsumer extends React.Component< + K, + T +> { + public static contextType = DialogStackContext + public declare context: React.ContextType + + protected checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { + if (isTopMost) { + this.onDialogIsTopMost() + } else { + this.onDialogIsNotTopMost() + } + }) + + /** The method called when the dialog is the top most in the stack. */ + protected abstract onDialogIsTopMost(): void + + /** The method called when the dialog is not top most in the stack. */ + protected abstract onDialogIsNotTopMost(): void + + public componentDidUpdate( + _prevProps?: Readonly, + _prevState?: Readonly<{}>, + _snapshot?: any + ): void { + this.checkWhetherDialogIsTopMost(this.context.isTopMost) + } + + public componentDidMount() { + this.checkWhetherDialogIsTopMost(this.context.isTopMost) + } + + public componentWillUnmount() { + this.onDialogIsNotTopMost() + } +} diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index 54b5f7f626..f9cd825ce5 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -4,31 +4,7 @@ import { DialogHeader } from './header' import { createUniqueId, releaseUniqueId } from '../lib/id-pool' import { getTitleBarHeight } from '../window/title-bar' import memoizeOne from 'memoize-one' - -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({ - isTopMost: false, -}) +import { DialogStackContextConsumer } from './dialog-stack-context-consumer' /** * The time (in milliseconds) from when the dialog is mounted @@ -163,27 +139,27 @@ interface IDialogState { * underlying elements. It's not possible to use the tab key to move focus * out of the dialog without first dismissing it. */ -export class Dialog extends React.Component { - public static contextType = DialogStackContext - public declare context: React.ContextType - +export class Dialog extends DialogStackContextConsumer< + IDialogProps, + IDialogState +> { private dialogElement: HTMLDialogElement | null = null private dismissGraceTimeoutId?: number private disableClickDismissalTimeoutId: number | null = null private disableClickDismissal = false - private checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { + protected checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { if (this.dialogElement == null) { return } if (isTopMost && !this.dialogElement.open) { - this.onDialogIsTopMost(this.dialogElement) + this.onDialogIsTopMost() } if (!isTopMost && this.dialogElement.open) { - this.onDialogIsNotTopMost(this.dialogElement) + this.onDialogIsNotTopMost() } }) @@ -291,16 +267,16 @@ export class Dialog extends React.Component { this.updateTitleId() } - public componentDidMount() { - this.checkWhetherDialogIsTopMost(this.context.isTopMost) - } + protected onDialogIsTopMost() { + if (this.dialogElement == null) { + return + } - private onDialogIsTopMost(dialogElement: HTMLDialogElement) { - dialogElement.showModal() + this.dialogElement.showModal() // Provide an event that components can subscribe to in order to perform // tasks such as re-layout after the dialog is visible - dialogElement.dispatchEvent( + this.dialogElement.dispatchEvent( new CustomEvent('dialog-show', { bubbles: true, cancelable: false, @@ -314,12 +290,15 @@ export class Dialog extends React.Component { window.addEventListener('focus', this.onWindowFocus) - this.resizeObserver.observe(dialogElement) + this.resizeObserver.observe(this.dialogElement) window.addEventListener('resize', this.scheduleResizeEvent) } - private onDialogIsNotTopMost(dialogElement?: HTMLDialogElement) { - dialogElement?.close() + protected onDialogIsNotTopMost() { + if (this.dialogElement !== null) { + this.dialogElement?.close() + return + } this.clearDismissGraceTimeout() @@ -483,7 +462,8 @@ export class Dialog extends React.Component { if (this.state.titleId) { releaseUniqueId(this.state.titleId) } - this.onDialogIsNotTopMost() + + super.componentWillUnmount() } public componentDidUpdate() { @@ -491,7 +471,7 @@ export class Dialog extends React.Component { this.updateTitleId() } - this.checkWhetherDialogIsTopMost(this.context.isTopMost) + super.componentDidUpdate() } private onDialogCancel = (e: Event | React.SyntheticEvent) => { From d7bb19f00ce59c585ba470bf9b2b19ae9fd9f398 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 28 Nov 2022 11:39:07 -0500 Subject: [PATCH 214/262] Linter --- app/src/ui/clone-repository/clone-repository.tsx | 2 +- app/src/ui/dialog/dialog-stack-context-consumer.tsx | 6 +----- app/src/ui/dialog/dialog.tsx | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/ui/clone-repository/clone-repository.tsx b/app/src/ui/clone-repository/clone-repository.tsx index 34a1c50abb..6c6bd4fb48 100644 --- a/app/src/ui/clone-repository/clone-repository.tsx +++ b/app/src/ui/clone-repository/clone-repository.tsx @@ -194,7 +194,7 @@ export class CloneRepository extends DialogStackContextConsumer< this.updateUrl(this.props.initialURL || '') } - super.componentDidUpdate() + super.componentDidUpdate(prevProps) } public componentDidMount() { diff --git a/app/src/ui/dialog/dialog-stack-context-consumer.tsx b/app/src/ui/dialog/dialog-stack-context-consumer.tsx index 76ebf3fd0a..c99a687a78 100644 --- a/app/src/ui/dialog/dialog-stack-context-consumer.tsx +++ b/app/src/ui/dialog/dialog-stack-context-consumer.tsx @@ -56,11 +56,7 @@ export abstract class DialogStackContextConsumer extends React.Component< /** The method called when the dialog is not top most in the stack. */ protected abstract onDialogIsNotTopMost(): void - public componentDidUpdate( - _prevProps?: Readonly, - _prevState?: Readonly<{}>, - _snapshot?: any - ): void { + public componentDidUpdate(prevProps: K): void { this.checkWhetherDialogIsTopMost(this.context.isTopMost) } diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index f9cd825ce5..d2a3843dc9 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -466,12 +466,12 @@ export class Dialog extends DialogStackContextConsumer< super.componentWillUnmount() } - public componentDidUpdate() { + public componentDidUpdate(prevProps: IDialogProps) { if (!this.props.title && this.state.titleId) { this.updateTitleId() } - super.componentDidUpdate() + super.componentDidUpdate(prevProps) } private onDialogCancel = (e: Event | React.SyntheticEvent) => { From d44c5d90c99b6d8403d9470c3dd7a9888573112c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Nov 2022 13:44:26 +0000 Subject: [PATCH 215/262] Bump minimatch from 3.0.4 to 3.1.2 in /app Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2. - [Release notes](https://github.com/isaacs/minimatch/releases) - [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2) --- updated-dependencies: - dependency-name: minimatch dependency-type: indirect ... Signed-off-by: dependabot[bot] --- app/yarn.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/yarn.lock b/app/yarn.lock index 4458395bd4..cc5a1aa065 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -119,9 +119,9 @@ babel-runtime@^6.26.0: regenerator-runtime "^0.11.0" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== bl@^3.0.0: version "3.0.1" @@ -131,9 +131,9 @@ bl@^3.0.0: readable-stream "^3.0.1" brace-expansion@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" - integrity sha1-wHshHHyVLsH479Uad+8NHTmQopI= + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -243,7 +243,7 @@ compare-versions@^3.6.0: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" @@ -854,9 +854,9 @@ mimic-response@^3.1.0: integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" From 213df72ee661bf1ecb8fcb5bd73163482a89738a Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 29 Nov 2022 08:45:06 -0500 Subject: [PATCH 216/262] Pass prop/add reused logic service --- app/src/ui/about/about.tsx | 38 +++++----- .../ui/add-repository/create-repository.tsx | 27 +++++-- app/src/ui/app.tsx | 20 ++--- .../ui/clone-repository/clone-repository.tsx | 33 ++++++--- .../dialog/dialog-stack-context-consumer.tsx | 70 ------------------ app/src/ui/dialog/dialog.tsx | 74 ++++++++++++------- app/src/ui/dialog/is-top-most-service.tsx | 20 +++++ 7 files changed, 141 insertions(+), 141 deletions(-) delete mode 100644 app/src/ui/dialog/dialog-stack-context-consumer.tsx create mode 100644 app/src/ui/dialog/is-top-most-service.tsx diff --git a/app/src/ui/about/about.tsx b/app/src/ui/about/about.tsx index 481703b1ad..5f6db61795 100644 --- a/app/src/ui/about/about.tsx +++ b/app/src/ui/about/about.tsx @@ -16,7 +16,7 @@ import { RelativeTime } from '../relative-time' import { assertNever } from '../../lib/fatal-error' import { ReleaseNotesUri } from '../lib/releases' import { encodePathAsUrl } from '../../lib/path' -import { DialogStackContextConsumer } from '../dialog/dialog-stack-context-consumer' +import { IsTopMostService } from '../dialog/is-top-most-service' const logoPath = __DARWIN__ ? 'static/logo-64x64@2x.png' @@ -55,6 +55,9 @@ interface IAboutProps { /** A function to call when the user wants to see Terms and Conditions. */ readonly onShowTermsAndConditions: () => void + + /** Whether the dialog is the top most in the dialog stack */ + readonly isTopMost: boolean } interface IAboutState { @@ -66,11 +69,18 @@ interface IAboutState { * A dialog that presents information about the * running application such as name and version. */ -export class About extends DialogStackContextConsumer< - IAboutProps, - IAboutState -> { +export class About extends React.Component { private updateStoreEventHandle: Disposable | null = null + private isTopMostService: IsTopMostService = new IsTopMostService( + () => { + window.addEventListener('keydown', this.onKeyDown) + window.addEventListener('keyup', this.onKeyUp) + }, + () => { + window.removeEventListener('keydown', this.onKeyDown) + window.removeEventListener('keyup', this.onKeyUp) + } + ) public constructor(props: IAboutProps) { super(props) @@ -90,7 +100,11 @@ export class About extends DialogStackContextConsumer< this.onUpdateStateChanged ) this.setState({ updateState: updateStore.state }) - super.componentDidMount() + this.isTopMostService.check(this.props.isTopMost) + } + + public componentDidUpdate(): void { + this.isTopMostService.check(this.props.isTopMost) } public componentWillUnmount() { @@ -98,17 +112,7 @@ export class About extends DialogStackContextConsumer< this.updateStoreEventHandle.dispose() this.updateStoreEventHandle = null } - super.componentWillUnmount() - } - - protected onDialogIsTopMost() { - window.addEventListener('keydown', this.onKeyDown) - window.addEventListener('keyup', this.onKeyUp) - } - - protected onDialogIsNotTopMost() { - window.removeEventListener('keydown', this.onKeyDown) - window.removeEventListener('keyup', this.onKeyUp) + this.isTopMostService.unmount() } private onKeyDown = (event: KeyboardEvent) => { diff --git a/app/src/ui/add-repository/create-repository.tsx b/app/src/ui/add-repository/create-repository.tsx index c9ad9ab988..9ff0369555 100644 --- a/app/src/ui/add-repository/create-repository.tsx +++ b/app/src/ui/add-repository/create-repository.tsx @@ -36,7 +36,7 @@ import { mkdir } from 'fs/promises' import { directoryExists } from '../../lib/directory-exists' import { FoldoutType } from '../../lib/app-state' import { join } from 'path' -import { DialogStackContextConsumer } from '../dialog/dialog-stack-context-consumer' +import { IsTopMostService } from '../dialog/is-top-most-service' /** The sentinel value used to indicate no gitignore should be used. */ const NoGitIgnoreValue = 'None' @@ -72,6 +72,9 @@ interface ICreateRepositoryProps { /** Prefills path input so user doesn't have to. */ readonly initialPath?: string + + /** Whether the dialog is the top most in the dialog stack */ + readonly isTopMost: boolean } interface ICreateRepositoryState { @@ -112,10 +115,20 @@ interface ICreateRepositoryState { } /** The Create New Repository component. */ -export class CreateRepository extends DialogStackContextConsumer< +export class CreateRepository extends React.Component< ICreateRepositoryProps, ICreateRepositoryState > { + private isTopMostService: IsTopMostService = new IsTopMostService( + () => { + this.updateReadMeExists(this.state.path, this.state.name) + window.addEventListener('focus', this.onWindowFocus) + }, + () => { + window.removeEventListener('focus', this.onWindowFocus) + } + ) + public constructor(props: ICreateRepositoryProps) { super(props) @@ -146,7 +159,7 @@ export class CreateRepository extends DialogStackContextConsumer< } public async componentDidMount() { - super.componentDidMount() + this.isTopMostService.check(this.props.isTopMost) const gitIgnoreNames = await getGitIgnoreNames() const licenses = await getLicenses() @@ -159,12 +172,12 @@ export class CreateRepository extends DialogStackContextConsumer< this.updateReadMeExists(path, this.state.name) } - protected onDialogIsTopMost() { - window.addEventListener('focus', this.onWindowFocus) + public componentDidUpdate(): void { + this.isTopMostService.check(this.props.isTopMost) } - protected onDialogIsNotTopMost() { - window.removeEventListener('focus', this.onWindowFocus) + public componentWillUnmount(): void { + this.isTopMostService.unmount() } private initializePath = async () => { diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index e6a6464906..58d4281e61 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -165,11 +165,8 @@ 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 { - DialogStackContext, - IDialogStackContext, -} from './dialog/dialog-stack-context-consumer' import { enableStackedPopups } from '../lib/feature-flag' +import { DialogStackContext } from './dialog' const MinuteInMilliseconds = 1000 * 60 const HourInMilliseconds = MinuteInMilliseconds * 60 @@ -1416,15 +1413,15 @@ export class App extends React.Component { return ( <> {allPopups.map(popup => { - const dialogStackContext: IDialogStackContext = { - isTopMost: this.state.currentPopup?.id === popup.id, - } + const isTopMost = this.state.currentPopup?.id === popup.id return ( - {this.popupContent(popup)} + {this.popupContent(popup, isTopMost)} ) })} @@ -1432,7 +1429,7 @@ export class App extends React.Component { ) } - private popupContent(popup: Popup): JSX.Element | null { + 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( @@ -1607,6 +1604,7 @@ export class App extends React.Component { onDismissed={onPopupDismissedFn} dispatcher={this.props.dispatcher} initialPath={popup.path} + isTopMost={isTopMost} /> ) case PopupType.CloneRepository: @@ -1622,6 +1620,7 @@ export class App extends React.Component { onTabSelected={this.onCloneRepositoriesTabSelected} apiRepositories={this.state.apiRepositories} onRefreshRepositories={this.onRefreshRepositories} + isTopMost={isTopMost} /> ) case PopupType.CreateBranch: { @@ -1682,6 +1681,7 @@ export class App extends React.Component { onCheckForNonStaggeredUpdates={this.onCheckForNonStaggeredUpdates} onShowAcknowledgements={this.showAcknowledgements} onShowTermsAndConditions={this.showTermsAndConditions} + isTopMost={isTopMost} /> ) case PopupType.PublishRepository: diff --git a/app/src/ui/clone-repository/clone-repository.tsx b/app/src/ui/clone-repository/clone-repository.tsx index 6c6bd4fb48..8ab4d4ee0e 100644 --- a/app/src/ui/clone-repository/clone-repository.tsx +++ b/app/src/ui/clone-repository/clone-repository.tsx @@ -24,7 +24,7 @@ import { ClickSource } from '../lib/list' import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' import { showOpenDialog, showSaveDialog } from '../main-process-proxy' import { readdir } from 'fs/promises' -import { DialogStackContextConsumer } from '../dialog/dialog-stack-context-consumer' +import { IsTopMostService } from '../dialog/is-top-most-service' interface ICloneRepositoryProps { readonly dispatcher: Dispatcher @@ -66,6 +66,9 @@ interface ICloneRepositoryProps { * available for cloning. */ readonly onRefreshRepositories: (account: Account) => void + + /** Whether the dialog is the top most in the dialog stack */ + readonly isTopMost: boolean } interface ICloneRepositoryState { @@ -145,10 +148,20 @@ interface IGitHubTabState extends IBaseTabState { } /** The component for cloning a repository. */ -export class CloneRepository extends DialogStackContextConsumer< +export class CloneRepository extends React.Component< ICloneRepositoryProps, ICloneRepositoryState > { + private isTopMostService: IsTopMostService = new IsTopMostService( + () => { + this.validatePath() + window.addEventListener('focus', this.onWindowFocus) + }, + () => { + window.removeEventListener('focus', this.onWindowFocus) + } + ) + public constructor(props: ICloneRepositoryProps) { super(props) @@ -194,7 +207,7 @@ export class CloneRepository extends DialogStackContextConsumer< this.updateUrl(this.props.initialURL || '') } - super.componentDidUpdate(prevProps) + this.isTopMostService.check(this.props.isTopMost) } public componentDidMount() { @@ -203,7 +216,11 @@ export class CloneRepository extends DialogStackContextConsumer< this.updateUrl(initialURL) } - super.componentDidMount() + this.isTopMostService.check(this.props.isTopMost) + } + + public componentWillUnmount(): void { + this.isTopMostService.unmount() } private initializePath = async () => { @@ -227,14 +244,6 @@ export class CloneRepository extends DialogStackContextConsumer< this.updateUrl(selectedTabState.url) } - protected onDialogIsTopMost() { - window.addEventListener('focus', this.onWindowFocus) - } - - protected onDialogIsNotTopMost() { - window.removeEventListener('focus', this.onWindowFocus) - } - public render() { const { error } = this.getSelectedTabState() return ( diff --git a/app/src/ui/dialog/dialog-stack-context-consumer.tsx b/app/src/ui/dialog/dialog-stack-context-consumer.tsx deleted file mode 100644 index c99a687a78..0000000000 --- a/app/src/ui/dialog/dialog-stack-context-consumer.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import memoizeOne from 'memoize-one' -import * as React from 'react' - -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({ - isTopMost: false, -}) - -/** - * A base component for any dialogs that consume the dialog stack context. - * - * This houses logic to respond to when the `isTopMost` changes on the - * `DialogStackContext` by providing two abstract methods of `onDialogIsTopMost` - * and `onDialogIsNotTopMost` and implementing a `checkWhetherDialogIsTopMost - * method that called via the components implementations of React component - * lifecycle methods. - */ -export abstract class DialogStackContextConsumer extends React.Component< - K, - T -> { - public static contextType = DialogStackContext - public declare context: React.ContextType - - protected checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { - if (isTopMost) { - this.onDialogIsTopMost() - } else { - this.onDialogIsNotTopMost() - } - }) - - /** The method called when the dialog is the top most in the stack. */ - protected abstract onDialogIsTopMost(): void - - /** The method called when the dialog is not top most in the stack. */ - protected abstract onDialogIsNotTopMost(): void - - public componentDidUpdate(prevProps: K): void { - this.checkWhetherDialogIsTopMost(this.context.isTopMost) - } - - public componentDidMount() { - this.checkWhetherDialogIsTopMost(this.context.isTopMost) - } - - public componentWillUnmount() { - this.onDialogIsNotTopMost() - } -} diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index d2a3843dc9..b3c599e791 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -3,8 +3,32 @@ import classNames from 'classnames' import { DialogHeader } from './header' import { createUniqueId, releaseUniqueId } from '../lib/id-pool' import { getTitleBarHeight } from '../window/title-bar' -import memoizeOne from 'memoize-one' -import { DialogStackContextConsumer } from './dialog-stack-context-consumer' +import { IsTopMostService } from './is-top-most-service' + +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({ + isTopMost: false, +}) /** * The time (in milliseconds) from when the dialog is mounted @@ -139,30 +163,25 @@ interface IDialogState { * underlying elements. It's not possible to use the tab key to move focus * out of the dialog without first dismissing it. */ -export class Dialog extends DialogStackContextConsumer< - IDialogProps, - IDialogState -> { +export class Dialog extends React.Component { + public static contextType = DialogStackContext + public declare context: React.ContextType + + private isTopMostService: IsTopMostService = new IsTopMostService( + () => { + this.onDialogIsTopMost() + }, + () => { + this.onDialogIsNotTopMost() + } + ) + private dialogElement: HTMLDialogElement | null = null private dismissGraceTimeoutId?: number private disableClickDismissalTimeoutId: number | null = null private disableClickDismissal = false - protected checkWhetherDialogIsTopMost = memoizeOne((isTopMost: boolean) => { - if (this.dialogElement == null) { - return - } - - if (isTopMost && !this.dialogElement.open) { - this.onDialogIsTopMost() - } - - if (!isTopMost && this.dialogElement.open) { - this.onDialogIsNotTopMost() - } - }) - /** * Resize observer used for tracking width changes and * refreshing the internal codemirror instance when @@ -267,12 +286,18 @@ export class Dialog extends DialogStackContextConsumer< this.updateTitleId() } + public componentDidMount() { + this.isTopMostService.check(this.context.isTopMost) + } + protected onDialogIsTopMost() { if (this.dialogElement == null) { return } - this.dialogElement.showModal() + if (!this.dialogElement.open) { + this.dialogElement.showModal() + } // Provide an event that components can subscribe to in order to perform // tasks such as re-layout after the dialog is visible @@ -295,9 +320,8 @@ export class Dialog extends DialogStackContextConsumer< } protected onDialogIsNotTopMost() { - if (this.dialogElement !== null) { + if (this.dialogElement !== null && this.dialogElement.open) { this.dialogElement?.close() - return } this.clearDismissGraceTimeout() @@ -463,7 +487,7 @@ export class Dialog extends DialogStackContextConsumer< releaseUniqueId(this.state.titleId) } - super.componentWillUnmount() + this.isTopMostService.unmount() } public componentDidUpdate(prevProps: IDialogProps) { @@ -471,7 +495,7 @@ export class Dialog extends DialogStackContextConsumer< this.updateTitleId() } - super.componentDidUpdate(prevProps) + this.isTopMostService.check(this.context.isTopMost) } private onDialogCancel = (e: Event | React.SyntheticEvent) => { diff --git a/app/src/ui/dialog/is-top-most-service.tsx b/app/src/ui/dialog/is-top-most-service.tsx new file mode 100644 index 0000000000..20d8aa5c7a --- /dev/null +++ b/app/src/ui/dialog/is-top-most-service.tsx @@ -0,0 +1,20 @@ +import memoizeOne from 'memoize-one' + +export class IsTopMostService { + public check = memoizeOne((isTopMost: boolean) => { + if (isTopMost) { + this.onDialogIsTopMost() + } else { + this.onDialogIsNotTopMost() + } + }) + + public constructor( + private onDialogIsTopMost: () => void, + private onDialogIsNotTopMost: () => void + ) {} + + public unmount() { + this.onDialogIsNotTopMost() + } +} From 0c0bb1bb1175d57ac4f7c0a7d9f680794dd9512b Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 29 Nov 2022 15:28:40 -0500 Subject: [PATCH 217/262] Things can only be dragged for Left clicks --- app/src/ui/lib/draggable.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/ui/lib/draggable.tsx b/app/src/ui/lib/draggable.tsx index 5357e927d5..5dbe91712a 100644 --- a/app/src/ui/lib/draggable.tsx +++ b/app/src/ui/lib/draggable.tsx @@ -51,10 +51,28 @@ export class Draggable extends React.Component { 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): boolean { - // right clicks or shift clicks const isSpecialClick = - event.button === 2 || + event.button !== 0 || (__DARWIN__ && event.button === 0 && event.ctrlKey) || event.shiftKey From 3c4aa37991cc66971e0c107e07dc7a1b8f7d668d Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 29 Nov 2022 15:46:45 -0500 Subject: [PATCH 218/262] Don't attempt to reopen the pull request dialog if already open --- app/src/lib/stores/app-store.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 728af6d5a5..2c26d7e3f0 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7377,6 +7377,10 @@ export class AppStore extends TypedBaseStore { ) } + if (this.popupManager.areTherePopupsOfType(PopupType.StartPullRequest)) { + return + } + const { allBranches, recentBranches, defaultBranch } = branchesState const { imageDiffType, selectedExternalEditor, showSideBySideDiff } = this.getState() From c947d024b59d21e8eb55fedd23e4990af556d838 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 30 Nov 2022 07:29:45 -0500 Subject: [PATCH 219/262] Use preview pull request item state - not create pr --- app/src/ui/changes/no-changes.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/ui/changes/no-changes.tsx b/app/src/ui/changes/no-changes.tsx index b8aad1cc84..19eec2e653 100644 --- a/app/src/ui/changes/no-changes.tsx +++ b/app/src/ui/changes/no-changes.tsx @@ -688,9 +688,9 @@ export class NoChanges extends React.Component< ) } - const startMenuItem = this.getMenuItemInfo('preview-pull-request') + const previewPullMenuItem = this.getMenuItemInfo('preview-pull-request') - if (startMenuItem === undefined) { + if (previewPullMenuItem === undefined) { log.error(`Could not find matching menu item for 'preview-pull-request'`) return null } @@ -722,8 +722,8 @@ export class NoChanges extends React.Component< value: PullRequestSuggestedNextAction.PreviewPullRequest, menuItemId: 'preview-pull-request', discoverabilityContent: - this.renderDiscoverabilityElements(startMenuItem), - disabled: !createMenuItem.enabled, + this.renderDiscoverabilityElements(previewPullMenuItem), + disabled: !previewPullMenuItem.enabled, } return ( From 5e87f8a4c8073822035f56fb66598383d1c9f34f Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 30 Nov 2022 13:55:22 -0500 Subject: [PATCH 220/262] If base branch isn't default, open GitHub to compare against that base --- app/src/lib/stores/app-store.ts | 31 ++++++++++++++----- app/src/ui/dispatcher/dispatcher.ts | 7 +++-- .../open-pull-request-dialog.tsx | 3 +- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 728af6d5a5..1e58d929bf 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -5958,7 +5958,10 @@ export class AppStore extends TypedBaseStore { await this._openInBrowser(url.toString()) } - public async _createPullRequest(repository: Repository): Promise { + public async _createPullRequest( + repository: Repository, + baseBranch?: Branch + ): Promise { const gitHubRepository = repository.gitHubRepository if (!gitHubRepository) { return @@ -5971,24 +5974,28 @@ export class AppStore extends TypedBaseStore { return } - const branch = tip.branch + const compareBranch = tip.branch const aheadBehind = state.aheadBehind if (aheadBehind == null) { this._showPopup({ type: PopupType.PushBranchCommits, repository, - branch, + branch: compareBranch, }) } else if (aheadBehind.ahead > 0) { this._showPopup({ type: PopupType.PushBranchCommits, repository, - branch, + branch: compareBranch, unPushedCommits: aheadBehind.ahead, }) } else { - await this._openCreatePullRequestInBrowser(repository, branch) + await this._openCreatePullRequestInBrowser( + repository, + compareBranch, + baseBranch + ) } } @@ -6083,15 +6090,23 @@ export class AppStore extends TypedBaseStore { public async _openCreatePullRequestInBrowser( repository: Repository, - branch: Branch + compareBranch: Branch, + baseBranch?: Branch ): Promise { const gitHubRepository = repository.gitHubRepository if (!gitHubRepository) { return } - const urlEncodedBranchName = encodeURIComponent(branch.nameWithoutRemote) - const baseURL = `${gitHubRepository.htmlURL}/pull/new/${urlEncodedBranchName}` + const encodedBaseBranch = + baseBranch !== undefined + ? encodeURIComponent(baseBranch.nameWithoutRemote) + '...' + : '' + const encodedCompareBranch = encodeURIComponent( + compareBranch.nameWithoutRemote + ) + const compareString = `${encodedBaseBranch}${encodedCompareBranch}` + const baseURL = `${gitHubRepository.htmlURL}/pull/new/${compareString}` await this._openInBrowser(baseURL) diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index f85bf4e766..dc747c8986 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -2193,8 +2193,11 @@ export class Dispatcher { * openCreatePullRequestInBrowser method which immediately opens the * create pull request page without showing a dialog. */ - public createPullRequest(repository: Repository): Promise { - return this.appStore._createPullRequest(repository) + public createPullRequest( + repository: Repository, + baseBranch?: Branch + ): Promise { + return this.appStore._createPullRequest(repository, baseBranch) } /** diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 4a470907dd..a56c6ad353 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -69,7 +69,8 @@ interface IOpenPullRequestDialogProps { /** The component for start a pull request. */ export class OpenPullRequestDialog extends React.Component { private onCreatePullRequest = () => { - this.props.dispatcher.createPullRequest(this.props.repository) + const { baseBranch } = this.props.pullRequestState + this.props.dispatcher.createPullRequest(this.props.repository, baseBranch) // TODO: create pr from dialog pr stat? this.props.dispatcher.recordCreatePullRequest() } From 3461fbe5622466ffc9555ccc0ef6aa688d535974 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Fri, 2 Dec 2022 11:56:55 +0100 Subject: [PATCH 221/262] Position hunk handle properly --- app/src/ui/diff/side-by-side-diff-row.tsx | 7 +++++++ app/styles/ui/_side-by-side-diff.scss | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/ui/diff/side-by-side-diff-row.tsx b/app/src/ui/diff/side-by-side-diff-row.tsx index 8aa385b462..2a3d75902c 100644 --- a/app/src/ui/diff/side-by-side-diff-row.tsx +++ b/app/src/ui/diff/side-by-side-diff-row.tsx @@ -436,6 +436,12 @@ export class SideBySideDiffRow extends React.Component< return null } + // In unified mode, the hunk handle left position depends on the line gutter + // width. + const style: React.CSSProperties = this.props.showSideBySideDiff + ? {} + : { left: this.lineGutterWidth } + return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
) } diff --git a/app/styles/ui/_side-by-side-diff.scss b/app/styles/ui/_side-by-side-diff.scss index 5e71cbe9e6..ba805c7fe2 100644 --- a/app/styles/ui/_side-by-side-diff.scss +++ b/app/styles/ui/_side-by-side-diff.scss @@ -314,6 +314,8 @@ } &.unified-diff { + --line-gutter-right-border-width: 4px; + .row { .before, .after { @@ -323,7 +325,8 @@ } .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 { @@ -365,10 +368,10 @@ &.editable .row { .line-number { - border-right-width: 4px; + border-right-width: var(--line-gutter-right-border-width); } .hunk-expansion-handle { - border-right-width: 4px; + border-right-width: var(--line-gutter-right-border-width); } } } From 6d04f734cc7ef3c14e095951d253887600186eed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Dec 2022 15:39:50 +0000 Subject: [PATCH 222/262] Bump decode-uri-component from 0.2.0 to 0.2.2 Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2. - [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases) - [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2) --- updated-dependencies: - dependency-name: decode-uri-component dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d521ba5396..8cd9ca5517 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3270,9 +3270,9 @@ decimal.js@^10.2.1: integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== decompress-response@^3.3.0: version "3.3.0" From 4f494dde7771d3e3d3ae3b011e796139b6b4940c Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 28 Nov 2022 18:32:30 -0500 Subject: [PATCH 223/262] Close dialog on PR Preview --- app/src/ui/open-pull-request/open-pull-request-dialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 4a470907dd..f63b215172 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -72,6 +72,7 @@ export class OpenPullRequestDialog extends React.Component { From c771c1b0f6b74e6e74b70856719d2f7370de207d Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 28 Nov 2022 19:01:04 -0500 Subject: [PATCH 224/262] Create button is a view button if pr already exists --- app/src/lib/stores/app-store.ts | 4 +- app/src/models/popup.ts | 1 + app/src/ui/app.tsx | 2 + .../open-pull-request-dialog.tsx | 44 ++++++++++++++----- app/styles/ui/dialogs/_open-pull-request.scss | 6 +++ 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 2c26d7e3f0..8c855b47d1 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7381,7 +7381,8 @@ export class AppStore extends TypedBaseStore { return } - const { allBranches, recentBranches, defaultBranch } = branchesState + const { allBranches, recentBranches, defaultBranch, currentPullRequest } = + branchesState const { imageDiffType, selectedExternalEditor, showSideBySideDiff } = this.getState() @@ -7399,6 +7400,7 @@ export class AppStore extends TypedBaseStore { ? commitSHAs[0] : null, showSideBySideDiff, + currentBranchHasPullRequest: currentPullRequest !== null, }) } diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index b2de01f356..85ac427f71 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -380,6 +380,7 @@ export type PopupDetail = repository: Repository nonLocalCommitSHA: string | null showSideBySideDiff: boolean + currentBranchHasPullRequest: boolean } | { type: PopupType.Error diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 03c596b3e6..a662252127 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2310,6 +2310,7 @@ export class App extends React.Component { recentBranches, repository, showSideBySideDiff, + currentBranchHasPullRequest, } = popup return ( @@ -2328,6 +2329,7 @@ export class App extends React.Component { repository={repository} externalEditorLabel={externalEditorLabel} showSideBySideDiff={showSideBySideDiff} + currentBranchHasPullRequest={currentBranchHasPullRequest} onDismissed={onPopupDismissedFn} /> ) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index f63b215172..7768309c4b 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -62,6 +62,9 @@ interface IOpenPullRequestDialogProps { * it's SHA */ readonly nonLocalCommitSHA: string | null + /** Whether the current branch already has a pull request*/ + readonly currentBranchHasPullRequest: boolean + /** Called to dismiss the dialog */ readonly onDismissed: () => void } @@ -69,10 +72,18 @@ interface IOpenPullRequestDialogProps { /** The component for start a pull request. */ export class OpenPullRequestDialog extends React.Component { private onCreatePullRequest = () => { - this.props.dispatcher.createPullRequest(this.props.repository) - // TODO: create pr from dialog pr stat? - this.props.dispatcher.recordCreatePullRequest() - this.props.onDismissed() + const { currentBranchHasPullRequest, dispatcher, repository, onDismissed } = + this.props + + if (currentBranchHasPullRequest) { + dispatcher.showPullRequest(repository) + } else { + dispatcher.createPullRequest(repository) + // TODO: create pr from dialog pr stat? + dispatcher.recordCreatePullRequest() + } + + onDismissed() } private onBranchChange = (branch: Branch) => { @@ -180,21 +191,34 @@ export class OpenPullRequestDialog extends React.Component + {currentBranchHasPullRequest && ( + + )} + {__DARWIN__ + ? `${viewCreate} Pull Request` + : `${viewCreate} pull request`} + + ) + return ( Date: Wed, 30 Nov 2022 14:15:39 -0500 Subject: [PATCH 225/262] If not hosted on github, no submit button needed --- .../open-pull-request-dialog.tsx | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 7768309c4b..53225a3f83 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -13,6 +13,8 @@ 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' +import { CloningRepository } from '../../models/cloning-repository' +import { Button } from '../lib/button' interface IOpenPullRequestDialogProps { readonly repository: Repository @@ -194,6 +196,7 @@ export class OpenPullRequestDialog extends React.Component - + {isHostedOnGitHub && ( + + )} + {!isHostedOnGitHub && } ) } @@ -241,3 +247,17 @@ export class OpenPullRequestDialog extends React.Component Date: Tue, 6 Dec 2022 10:47:51 -0500 Subject: [PATCH 226/262] Use `isRepositoryWithGitHubRepository` Co-Authored-By: Markus Olsson <634063+niik@users.noreply.github.com> --- .../open-pull-request-dialog.tsx | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 53225a3f83..d8e84d2fc1 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -3,7 +3,10 @@ import { IConstrainedValue, IPullRequestState } from '../../lib/app-state' import { getDotComAPIEndpoint } from '../../lib/api' import { Branch } from '../../models/branch' import { ImageDiffType } from '../../models/diff' -import { Repository } from '../../models/repository' +import { + isRepositoryWithGitHubRepository, + Repository, +} from '../../models/repository' import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog' import { Dispatcher } from '../dispatcher' import { Ref } from '../lib/ref' @@ -13,7 +16,6 @@ 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' -import { CloningRepository } from '../../models/cloning-repository' import { Button } from '../lib/button' interface IOpenPullRequestDialogProps { @@ -196,7 +198,7 @@ export class OpenPullRequestDialog extends React.Component Date: Wed, 30 Nov 2022 09:31:04 -0500 Subject: [PATCH 227/262] At least find a comparison branch --- app/src/lib/app-state.ts | 4 +- app/src/lib/stores/app-store.ts | 55 +++++++++++++++---- app/src/lib/stores/git-store.ts | 2 +- app/src/lib/stores/repository-state-cache.ts | 3 +- app/src/ui/branches/branch-select.tsx | 2 +- .../open-pull-request-dialog.tsx | 32 ++++++++++- .../open-pull-request-header.tsx | 2 +- app/styles/ui/dialogs/_open-pull-request.scss | 2 +- 8 files changed, 82 insertions(+), 20 deletions(-) diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index 69c32a28c9..7e0ababae8 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -967,7 +967,7 @@ export interface IPullRequestState { * The base branch of a a pull request - the branch the currently checked out * branch would merge into */ - readonly baseBranch: Branch + readonly baseBranch: Branch | null /** The SHAs of commits of the pull request */ readonly commitSHAs: ReadonlyArray | null @@ -981,7 +981,7 @@ export interface IPullRequestState { * repositories commit selection where the diff of all commits represents the * diff between the latest commit and the earliest commits parent. */ - readonly commitSelection: ICommitSelection + readonly commitSelection: ICommitSelection | null /** The result of merging the pull request branch into the base branch */ readonly mergeStatus: MergeTreeResult | null diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 2c26d7e3f0..9f21be7288 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7294,23 +7294,28 @@ export class AppStore extends TypedBaseStore { } public async _startPullRequest(repository: Repository) { - const { branchesState } = this.repositoryStateCache.get(repository) - const { defaultBranch, tip } = branchesState + const { tip, defaultBranch } = + this.repositoryStateCache.get(repository).branchesState - if (defaultBranch === null || tip.kind !== TipState.Valid) { + if (tip.kind !== TipState.Valid) { + // Shouldn't even be able to get here if so - just a type check return } + const currentBranch = tip.branch this._initializePullRequestPreview(repository, defaultBranch, currentBranch) } private async _initializePullRequestPreview( repository: Repository, - baseBranch: Branch, + baseBranch: Branch | null, currentBranch: Branch ) { - const { branchesState, localCommitSHAs } = - this.repositoryStateCache.get(repository) + if (baseBranch === null) { + this.showPullRequestPopupNoBaseBranch(repository, currentBranch) + return + } + const gitStore = this.gitStoreCache.get(repository) const pullRequestCommits = await gitStore.getCommitsBetweenBranches( @@ -7377,14 +7382,45 @@ export class AppStore extends TypedBaseStore { ) } + 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 + ) { if (this.popupManager.areTherePopupsOfType(PopupType.StartPullRequest)) { return } + const { branchesState, localCommitSHAs } = + this.repositoryStateCache.get(repository) const { allBranches, recentBranches, defaultBranch } = branchesState const { imageDiffType, selectedExternalEditor, showSideBySideDiff } = this.getState() + const nonLocalCommitSHA = + commitSHAs.length > 0 && !localCommitSHAs.includes(commitSHAs[0]) + ? commitSHAs[0] + : null + this._showPopup({ type: PopupType.StartPullRequest, allBranches, @@ -7394,10 +7430,7 @@ export class AppStore extends TypedBaseStore { recentBranches, repository, externalEditorLabel: selectedExternalEditor ?? undefined, - nonLocalCommitSHA: - commitSHAs.length > 0 && !localCommitSHAs.includes(commitSHAs[0]) - ? commitSHAs[0] - : null, + nonLocalCommitSHA, showSideBySideDiff, }) } @@ -7418,7 +7451,7 @@ export class AppStore extends TypedBaseStore { const currentBranch = branchesState.tip.branch const { baseBranch, commitSHAs } = pullRequestState - if (commitSHAs === null) { + if (commitSHAs === null || baseBranch === null) { return } diff --git a/app/src/lib/stores/git-store.ts b/app/src/lib/stores/git-store.ts index 84d367cea7..0f776db9f1 100644 --- a/app/src/lib/stores/git-store.ts +++ b/app/src/lib/stores/git-store.ts @@ -535,7 +535,7 @@ export class GitStore extends BaseStore { * using the available API data, remote information or branch * name conventions. */ - private async resolveDefaultBranch(): Promise { + public async resolveDefaultBranch(): Promise { if (this.currentRemote !== null) { // the Git server should use [remote]/HEAD to advertise // it's default branch, so see if it exists and matches diff --git a/app/src/lib/stores/repository-state-cache.ts b/app/src/lib/stores/repository-state-cache.ts index 79cbc11db0..929ad44dcb 100644 --- a/app/src/lib/stores/repository-state-cache.ts +++ b/app/src/lib/stores/repository-state-cache.ts @@ -286,7 +286,8 @@ export class RepositoryStateCache { } const oldState = pullRequestState.commitSelection - const commitSelection = merge(oldState, fn(oldState)) + const commitSelection = + oldState === null ? null : merge(oldState, fn(oldState)) this.updatePullRequestState(repository, () => ({ commitSelection, })) diff --git a/app/src/ui/branches/branch-select.tsx b/app/src/ui/branches/branch-select.tsx index e79b5c6742..662451dd18 100644 --- a/app/src/ui/branches/branch-select.tsx +++ b/app/src/ui/branches/branch-select.tsx @@ -9,7 +9,7 @@ import { IBranchListItem } from './group-branches' interface IBranchSelectProps { /** The initially selected branch. */ - readonly branch: Branch + readonly branch: Branch | null /** * See IBranchesState.defaultBranch diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 4a470907dd..16e54dde0e 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -106,6 +106,7 @@ export class OpenPullRequestDialog extends React.Component {this.renderNoChanges()} + {this.renderNoDefaultBranch()} {this.renderFilesChanged()}
) @@ -123,6 +124,11 @@ export class OpenPullRequestDialog extends React.Component ) return ( -
+

There are no changes.

@@ -178,6 +188,24 @@ export class OpenPullRequestDialog extends React.Component +
+ +

Could not find a default branch to compare against.

+ Select a base branch above. +
+
+ ) + } + private renderFooter() { const { mergeStatus, commitSHAs } = this.props.pullRequestState const gitHubRepository = this.props.repository.gitHubRepository diff --git a/app/src/ui/open-pull-request/open-pull-request-header.tsx b/app/src/ui/open-pull-request/open-pull-request-header.tsx index 6bb7bc97fd..ebc01f6595 100644 --- a/app/src/ui/open-pull-request/open-pull-request-header.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-header.tsx @@ -7,7 +7,7 @@ import { Ref } from '../lib/ref' interface IOpenPullRequestDialogHeaderProps { /** The base branch of the pull request */ - readonly baseBranch: Branch + readonly baseBranch: Branch | null /** The branch of the pull request */ readonly currentBranch: Branch diff --git a/app/styles/ui/dialogs/_open-pull-request.scss b/app/styles/ui/dialogs/_open-pull-request.scss index bb69524da2..fbe1eae5da 100644 --- a/app/styles/ui/dialogs/_open-pull-request.scss +++ b/app/styles/ui/dialogs/_open-pull-request.scss @@ -29,7 +29,7 @@ flex-grow: 1; } - .open-pull-request-no-changes { + .open-pull-request-message { height: 100%; display: flex; align-items: center; From d3e9fc8261f210e04dc432075a90924c82e2e568 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 7 Dec 2022 09:08:07 -0500 Subject: [PATCH 228/262] Add Preview Pull Request Metrics --- app/src/lib/stats/stats-database.ts | 6 ++++++ app/src/lib/stats/stats-store.ts | 21 +++++++++++++++++++ app/src/lib/stores/app-store.ts | 2 ++ app/src/ui/dispatcher/dispatcher.ts | 4 ++++ .../open-pull-request-dialog.tsx | 2 +- 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/src/lib/stats/stats-database.ts b/app/src/lib/stats/stats-database.ts index 49a02fa62d..57242cdf04 100644 --- a/app/src/lib/stats/stats-database.ts +++ b/app/src/lib/stats/stats-database.ts @@ -149,6 +149,9 @@ export interface IDailyMeasures { /** The number of times the user is taken to the create pull request page on dotcom */ readonly createPullRequestCount: number + /** The number of times the user is taken to the create pull request page on dotcom from the preview dialog */ + readonly createPullRequestFromPreviewCount: number + /** The number of times the rebase conflicts dialog is dismissed */ readonly rebaseConflictsDialogDismissalCount: number @@ -565,6 +568,9 @@ export interface IDailyMeasures { /** The number of times the user opens a submodule repository from its diff */ readonly openSubmoduleFromDiffCount: number + + /** The number of times a user has opened the preview pull request dialog */ + readonly previewedPullRequestCount: number } export class StatsDatabase extends Dexie { diff --git a/app/src/lib/stats/stats-store.ts b/app/src/lib/stats/stats-store.ts index a3540bde0b..f24562be9b 100644 --- a/app/src/lib/stats/stats-store.ts +++ b/app/src/lib/stats/stats-store.ts @@ -112,6 +112,7 @@ const DefaultDailyMeasures: IDailyMeasures = { guidedConflictedMergeCompletionCount: 0, unguidedConflictedMergeCompletionCount: 0, createPullRequestCount: 0, + createPullRequestFromPreviewCount: 0, rebaseConflictsDialogDismissalCount: 0, rebaseConflictsDialogReopenedCount: 0, rebaseAbortedAfterConflictsCount: 0, @@ -219,6 +220,7 @@ const DefaultDailyMeasures: IDailyMeasures = { submoduleDiffViewedFromChangesListCount: 0, submoduleDiffViewedFromHistoryCount: 0, openSubmoduleFromDiffCount: 0, + previewedPullRequestCount: 0, } interface IOnboardingStats { @@ -1075,6 +1077,16 @@ export class StatsStore implements IStatsStore { })) } + /** + * Increments the `createPullRequestFromPreviewCount` metric + */ + public recordCreatePullRequestFromPreview(): Promise { + return this.updateDailyMeasures(m => ({ + createPullRequestFromPreviewCount: + m.createPullRequestFromPreviewCount + 1, + })) + } + /** * Increments the `rebaseConflictsDialogDismissalCount` metric */ @@ -1981,6 +1993,15 @@ export class StatsStore implements IStatsStore { log.error(`Error reporting opt ${direction}:`, e) } } + + /** + * Increments the `createPullRequestFromPreviewCount` metric + */ + public recordPreviewedPullRequest(): Promise { + return this.updateDailyMeasures(m => ({ + previewedPullRequestCount: m.previewedPullRequestCount + 1, + })) + } } /** diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 8c855b47d1..fdbf6fc9ac 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7381,6 +7381,8 @@ export class AppStore extends TypedBaseStore { return } + this.statsStore.recordPreviewedPullRequest() + const { allBranches, recentBranches, defaultBranch, currentPullRequest } = branchesState const { imageDiffType, selectedExternalEditor, showSideBySideDiff } = diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index f85bf4e766..98573f6c62 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -2457,6 +2457,10 @@ export class Dispatcher { return this.statsStore.recordCreatePullRequest() } + public recordCreatePullRequestFromPreview() { + return this.statsStore.recordCreatePullRequestFromPreview() + } + public recordWelcomeWizardInitiated() { return this.statsStore.recordWelcomeWizardInitiated() } diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index d8e84d2fc1..371625a3d8 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -83,8 +83,8 @@ export class OpenPullRequestDialog extends React.Component Date: Wed, 7 Dec 2022 12:17:47 -0500 Subject: [PATCH 229/262] Update gitlab.md screenshots --- docs/integrations/gitlab.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/integrations/gitlab.md b/docs/integrations/gitlab.md index 0cba8d3d42..04103e9178 100644 --- a/docs/integrations/gitlab.md +++ b/docs/integrations/gitlab.md @@ -4,11 +4,11 @@ To authenticate against GitLab repositories you will need to create a personal access token. -1. Go to your GitLab account and select **Settings** in the user profile dropdown. +1. Go to your GitLab account and select **Edit Profile** in the user profile dropdown. -![](https://user-images.githubusercontent.com/721500/54834720-1f468a00-4c97-11e9-9a0f-4c92224064d0.png) +![](https://user-images.githubusercontent.com/721500/206245864-025fedb1-88e5-4c58-84dd-0d4b24eff76d.png) -2. Select **Access tokens** +2. In the left sidebar, select **Access tokens** 3. Under **Add a personal access token** choose a name and set an expiration date for your token. From 3f414bf51723a43b8051945910ecedb1fba65b34 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Fri, 9 Dec 2022 10:55:12 -0500 Subject: [PATCH 230/262] Local repos shouldn't be able to preview pull request --- app/src/lib/menu-update.ts | 2 +- .../open-pull-request-dialog.tsx | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/lib/menu-update.ts b/app/src/lib/menu-update.ts index 05dd9cb0b8..8e86919515 100644 --- a/app/src/lib/menu-update.ts +++ b/app/src/lib/menu-update.ts @@ -296,7 +296,7 @@ function getRepositoryMenuBuilder(state: IAppState): MenuStateBuilder { if (enableStartingPullRequests()) { menuStateBuilder.setEnabled( 'preview-pull-request', - !branchIsUnborn && !onDetachedHead + !branchIsUnborn && !onDetachedHead && isHostedOnGitHub ) } diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index d8e84d2fc1..3f91f457b2 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -198,7 +198,6 @@ export class OpenPullRequestDialog extends React.Component - {isHostedOnGitHub && ( - - )} - {!isHostedOnGitHub && } + + ) } From a689709ffdefa62c6f92693e0b4ca52770e946b1 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Fri, 9 Dec 2022 10:55:57 -0500 Subject: [PATCH 231/262] Use default remote branches for base branch options. --- app/src/lib/stores/app-store.ts | 16 +++++++++-- app/src/models/popup.ts | 4 +-- app/src/ui/app.tsx | 8 +++--- .../open-pull-request-dialog.tsx | 28 ++++++++++--------- .../open-pull-request-header.tsx | 22 +++++++++------ 5 files changed, 49 insertions(+), 29 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 8c855b47d1..a433e3e690 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7383,16 +7383,28 @@ export class AppStore extends TypedBaseStore { const { allBranches, recentBranches, defaultBranch, currentPullRequest } = branchesState + /* 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. + * + * Other notes: A repo can only create a pull request if it is hosted on + * dotcom. + */ + const prBaseBranches = allBranches.filter( + b => b.upstreamRemoteName === gitStore.defaultRemote?.name + ) + const prRecentBaseBranches = recentBranches.filter( + b => b.upstreamRemoteName === gitStore.defaultRemote?.name + ) const { imageDiffType, selectedExternalEditor, showSideBySideDiff } = this.getState() this._showPopup({ type: PopupType.StartPullRequest, - allBranches, + prBaseBranches, + prRecentBaseBranches, currentBranch, defaultBranch, imageDiffType, - recentBranches, repository, externalEditorLabel: selectedExternalEditor ?? undefined, nonLocalCommitSHA: diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 85ac427f71..7b21a8ca24 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -371,12 +371,12 @@ export type PopupDetail = } | { type: PopupType.StartPullRequest - allBranches: ReadonlyArray + prBaseBranches: ReadonlyArray currentBranch: Branch defaultBranch: Branch | null externalEditorLabel?: string imageDiffType: ImageDiffType - recentBranches: ReadonlyArray + prRecentBaseBranches: ReadonlyArray repository: Repository nonLocalCommitSHA: string | null showSideBySideDiff: boolean diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index a662252127..f48a9d4c99 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -2301,13 +2301,13 @@ export class App extends React.Component { this.state const { - allBranches, + prBaseBranches, currentBranch, defaultBranch, imageDiffType, externalEditorLabel, nonLocalCommitSHA, - recentBranches, + prRecentBaseBranches, repository, showSideBySideDiff, currentBranchHasPullRequest, @@ -2316,7 +2316,7 @@ export class App extends React.Component { return ( { imageDiffType={imageDiffType} nonLocalCommitSHA={nonLocalCommitSHA} pullRequestState={pullRequestState} - recentBranches={recentBranches} + prRecentBaseBranches={prRecentBaseBranches} repository={repository} externalEditorLabel={externalEditorLabel} showSideBySideDiff={showSideBySideDiff} diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 3f91f457b2..6f0e075c22 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -3,10 +3,7 @@ import { IConstrainedValue, IPullRequestState } from '../../lib/app-state' import { getDotComAPIEndpoint } from '../../lib/api' import { Branch } from '../../models/branch' import { ImageDiffType } from '../../models/diff' -import { - isRepositoryWithGitHubRepository, - Repository, -} from '../../models/repository' +import { Repository } from '../../models/repository' import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog' import { Dispatcher } from '../dispatcher' import { Ref } from '../lib/ref' @@ -16,7 +13,6 @@ 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' -import { Button } from '../lib/button' interface IOpenPullRequestDialogProps { readonly repository: Repository @@ -38,14 +34,20 @@ interface IOpenPullRequestDialogProps { readonly defaultBranch: Branch | null /** - * See IBranchesState.allBranches + * Branches in the repo with the repo's default remote + * + * We only want branches that are also on dotcom such that, when we ask a user + * to create a pull request, the base branch also exists on dotcom. */ - readonly allBranches: ReadonlyArray + readonly prBaseBranches: ReadonlyArray /** - * 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 + readonly prRecentBaseBranches: ReadonlyArray /** Whether we should display side by side diffs. */ readonly showSideBySideDiff: boolean @@ -100,8 +102,8 @@ export class OpenPullRequestDialog extends React.Component + readonly prBaseBranches: ReadonlyArray /** - * 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 + readonly prRecentBaseBranches: ReadonlyArray /** The count of commits of the pull request */ readonly commitCount: number @@ -73,8 +79,8 @@ export class OpenPullRequestDialogHeader extends React.Component< baseBranch, currentBranch, defaultBranch, - allBranches, - recentBranches, + prBaseBranches, + prRecentBaseBranches, commitCount, onBranchChange, onDismissed, @@ -95,8 +101,8 @@ export class OpenPullRequestDialogHeader extends React.Component< branch={baseBranch} defaultBranch={defaultBranch} currentBranch={currentBranch} - allBranches={allBranches} - recentBranches={recentBranches} + allBranches={prBaseBranches} + recentBranches={prRecentBaseBranches} onChange={onBranchChange} />{' '} from {currentBranch.name}. From 1bdc2969880f014ebf24051c420d2d66820d3051 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Dec 2022 14:50:01 +0000 Subject: [PATCH 232/262] Bump express from 4.17.0 to 4.17.3 Bumps [express](https://github.com/expressjs/express) from 4.17.0 to 4.17.3. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.17.0...4.17.3) --- updated-dependencies: - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 266 +++++++++++++++++++++------------------------------ 2 files changed, 109 insertions(+), 159 deletions(-) diff --git a/package.json b/package.json index 1212703dc9..7c345c3e80 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "eslint-plugin-json": "^2.1.1", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "7.26.1", - "express": "^4.15.0", + "express": "^4.17.3", "fake-indexeddb": "^2.0.4", "file-loader": "^6.2.0", "front-matter": "^2.3.0", diff --git a/yarn.lock b/yarn.lock index 8cd9ca5517..0f84f42d57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1811,13 +1811,13 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" + mime-types "~2.1.34" + negotiator "0.6.3" acorn-globals@^6.0.0: version "6.0.0" @@ -2472,21 +2472,21 @@ bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== +body-parser@1.19.2: + version "1.19.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" + integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== dependencies: - bytes "3.1.0" + bytes "3.1.2" content-type "~1.0.4" debug "2.6.9" depd "~1.1.2" - http-errors "1.7.2" + http-errors "1.8.1" iconv-lite "0.4.24" on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" + qs "6.9.7" + raw-body "2.4.3" + type-is "~1.6.18" boolbase@^1.0.0: version "1.0.0" @@ -2643,10 +2643,10 @@ builtin-modules@^1.0.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== cache-base@^1.0.1: version "1.0.1" @@ -3014,12 +3014,12 @@ console-polyfill@^0.3.0: resolved "https://registry.yarnpkg.com/console-polyfill/-/console-polyfill-0.3.0.tgz#84900902a18c47a5eba932be75fa44d23e8af861" integrity sha512-w+JSDZS7XML43Xnwo2x5O5vxB0ID7T5BdqDtyqT6uiCAX2kZAgcWxNaGqT97tZfSHzfOcvrfsDAodKcJ3UvnXQ== -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: - safe-buffer "5.1.2" + safe-buffer "5.2.1" content-type@~1.0.4: version "1.0.4" @@ -3043,10 +3043,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== copy-descriptor@^0.1.0: version "0.1.1" @@ -3205,55 +3205,20 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.3.4: +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87" - integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg== - dependencies: - ms "^2.1.1" - -debug@^4.1.0, debug@^4.1.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -debug@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== - dependencies: - ms "2.1.2" - -debug@^4.3.2, debug@^4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -4336,17 +4301,17 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" -express@^4.15.0: - version "4.17.0" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.0.tgz#288af62228a73f4c8ea2990ba3b791bb87cd4438" - integrity sha512-1Z7/t3Z5ZnBG252gKUPyItc4xdeaA0X934ca2ewckAsVsw9EG71i++ZHZPYnus8g/s5Bty8IMpSVEuRkmwwPRQ== +express@^4.17.3: + version "4.17.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" + integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== dependencies: - accepts "~1.3.7" + accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" + body-parser "1.19.2" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.4.0" + cookie "0.4.2" cookie-signature "1.0.6" debug "2.6.9" depd "~1.1.2" @@ -4360,13 +4325,13 @@ express@^4.15.0: on-finished "~2.3.0" parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" + proxy-addr "~2.0.7" + qs "6.9.7" range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" + safe-buffer "5.2.1" + send "0.17.2" + serve-static "1.14.2" + setprototypeof "1.2.0" statuses "~1.5.0" type-is "~1.6.18" utils-merge "1.0.1" @@ -4624,10 +4589,10 @@ form-data@~2.3.2: combined-stream "1.0.6" mime-types "^2.1.12" -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fragment-cache@^0.2.1: version "0.2.1" @@ -5217,16 +5182,16 @@ http-cache-semantics@^4.0.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== -http-errors@1.7.2, http-errors@~1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== +http-errors@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== dependencies: depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" + inherits "2.0.4" + setprototypeof "1.2.0" statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + toidentifier "1.0.1" http-proxy-agent@^4.0.1: version "4.0.1" @@ -5332,16 +5297,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - ini@^1.3.4: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -5371,10 +5331,10 @@ interpret@^1.0.1: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0" integrity sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA= -ipaddr.js@1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" - integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-accessor-descriptor@^0.1.6: version "0.1.6" @@ -7016,7 +6976,7 @@ mime-db@~1.36.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" integrity sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw== -mime-types@^2.1.12: +mime-types@^2.1.12, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -7154,17 +7114,12 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -7197,10 +7152,10 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== neo-async@^2.6.2: version "2.6.2" @@ -7972,13 +7927,13 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= -proxy-addr@~2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" - integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.0" + forwarded "0.2.0" + ipaddr.js "1.9.1" prr@~0.0.0: version "0.0.0" @@ -8020,10 +7975,10 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@6.9.7: + version "6.9.7" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" + integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== qs@~6.5.2: version "6.5.2" @@ -8059,13 +8014,13 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== +raw-body@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" + integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== dependencies: - bytes "3.1.0" - http-errors "1.7.2" + bytes "3.1.2" + http-errors "1.8.1" iconv-lite "0.4.24" unpipe "1.0.0" @@ -8521,21 +8476,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.1.2, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@^5.0.1, safe-buffer@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg== - -safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -8676,10 +8626,10 @@ semver@^7.3.7: dependencies: lru-cache "^6.0.0" -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== +send@0.17.2: + version "0.17.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" + integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== dependencies: debug "2.6.9" depd "~1.1.2" @@ -8688,9 +8638,9 @@ send@0.17.1: escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.7.2" + http-errors "1.8.1" mime "1.6.0" - ms "2.1.1" + ms "2.1.3" on-finished "~2.3.0" range-parser "~1.2.1" statuses "~1.5.0" @@ -8709,15 +8659,15 @@ serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== +serve-static@1.14.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" + integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.17.1" + send "0.17.2" set-blocking@^2.0.0: version "2.0.0" @@ -8761,10 +8711,10 @@ setimmediate@^1.0.5: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shallow-clone@^3.0.0: version "3.0.1" @@ -9477,10 +9427,10 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== totalist@^1.0.0: version "1.1.0" @@ -9671,7 +9621,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.17, type-is@~1.6.18: +type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== From 999c36960e8bd4976c11643c7788389d89c249b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 09:10:02 +0000 Subject: [PATCH 233/262] Bump qs from 6.5.2 to 6.5.3 Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.5.3. - [Release notes](https://github.com/ljharb/qs/releases) - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.5.2...v6.5.3) --- updated-dependencies: - dependency-name: qs dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0f84f42d57..ffa6ec57cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7981,9 +7981,9 @@ qs@6.9.7: integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== querystring@^0.2.0: version "0.2.0" From 7dcdc9c2dbb53bd134432b4e7d2d560d2639b3d6 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Mon, 12 Dec 2022 08:45:06 -0500 Subject: [PATCH 234/262] Update app/src/lib/stats/stats-store.ts Co-authored-by: Sergio Padrino --- app/src/lib/stats/stats-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/stats/stats-store.ts b/app/src/lib/stats/stats-store.ts index f24562be9b..dc5e3e3b7e 100644 --- a/app/src/lib/stats/stats-store.ts +++ b/app/src/lib/stats/stats-store.ts @@ -1995,7 +1995,7 @@ export class StatsStore implements IStatsStore { } /** - * Increments the `createPullRequestFromPreviewCount` metric + * Increments the `previewedPullRequestCount` metric */ public recordPreviewedPullRequest(): Promise { return this.updateDailyMeasures(m => ({ From 1143f949dafec2fa46bb9758931a72e18ad734e5 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 12 Dec 2022 08:49:32 -0500 Subject: [PATCH 235/262] Note about tracking create pull request for preview included --- app/src/lib/stats/stats-database.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/lib/stats/stats-database.ts b/app/src/lib/stats/stats-database.ts index 57242cdf04..8d3958a562 100644 --- a/app/src/lib/stats/stats-database.ts +++ b/app/src/lib/stats/stats-database.ts @@ -146,7 +146,11 @@ export interface IDailyMeasures { /** The number of times the user committed a conflicted merge outside the merge conflicts dialog */ readonly unguidedConflictedMergeCompletionCount: number - /** The number of times the user is taken to the create pull request page on dotcom */ + /** The number of times the user is taken to the create pull request page on dotcom including. + * + * NB - This metric tracks all times including when + * `createPullRequestFromPreviewCount` this is tracked. + * */ readonly createPullRequestCount: number /** The number of times the user is taken to the create pull request page on dotcom from the preview dialog */ From 6381545b9a76d1c8bab05385e5e159c3290370b2 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 12 Dec 2022 12:10:32 -0500 Subject: [PATCH 236/262] Consider Forks --- app/src/lib/stores/app-store.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index a433e3e690..e416471acd 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -8,6 +8,7 @@ import { PullRequestCoordinator, RepositoriesStore, SignInStore, + UpstreamRemoteName, } from '.' import { Account } from '../../models/account' import { AppMenu, IMenu } from '../../models/app-menu' @@ -46,6 +47,8 @@ import { isRepositoryWithGitHubRepository, RepositoryWithGitHubRepository, getNonForkGitHubRepository, + getForkContributionTarget, + isRepositoryWithForkedGitHubRepository, } from '../../models/repository' import { CommittedFileChange, @@ -263,7 +266,10 @@ import { parseRemote } from '../../lib/remote-parsing' import { createTutorialRepository } from './helpers/create-tutorial-repository' import { sendNonFatalException } from '../helpers/non-fatal-exception' import { getDefaultDir } from '../../ui/lib/default-dir' -import { WorkflowPreferences } from '../../models/workflow-preferences' +import { + ForkContributionTarget, + WorkflowPreferences, +} from '../../models/workflow-preferences' import { RepositoryIndicatorUpdater } from './helpers/repository-indicator-updater' import { isAttributableEmailFor } from '../email' import { TrashNameLabel } from '../../ui/lib/context-menu' @@ -7389,11 +7395,16 @@ export class AppStore extends TypedBaseStore { * Other notes: A repo can only create a pull request if it is hosted on * dotcom. */ + const remote = + isRepositoryWithForkedGitHubRepository(repository) && + getForkContributionTarget(repository) === ForkContributionTarget.Parent + ? UpstreamRemoteName + : gitStore.defaultRemote?.name const prBaseBranches = allBranches.filter( - b => b.upstreamRemoteName === gitStore.defaultRemote?.name + b => b.upstreamRemoteName === remote || b.remoteName === remote ) const prRecentBaseBranches = recentBranches.filter( - b => b.upstreamRemoteName === gitStore.defaultRemote?.name + b => b.upstreamRemoteName === remote || b.remoteName === remote ) const { imageDiffType, selectedExternalEditor, showSideBySideDiff } = this.getState() From c3c4dd5d8426e1fbcccf0551aa0a434096c49356 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 12 Dec 2022 12:47:41 -0500 Subject: [PATCH 237/262] Tidying up a little I think.. --- app/src/lib/stores/app-store.ts | 41 ++++++++++++++++++++------------- app/src/models/repository.ts | 12 ++++++++++ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 38e88e00f7..88ca985783 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -47,8 +47,7 @@ import { isRepositoryWithGitHubRepository, RepositoryWithGitHubRepository, getNonForkGitHubRepository, - getForkContributionTarget, - isRepositoryWithForkedGitHubRepository, + isRepositoryAForkContributingToParent, } from '../../models/repository' import { CommittedFileChange, @@ -266,10 +265,7 @@ import { parseRemote } from '../../lib/remote-parsing' import { createTutorialRepository } from './helpers/create-tutorial-repository' import { sendNonFatalException } from '../helpers/non-fatal-exception' import { getDefaultDir } from '../../ui/lib/default-dir' -import { - ForkContributionTarget, - WorkflowPreferences, -} from '../../models/workflow-preferences' +import { WorkflowPreferences } from '../../models/workflow-preferences' import { RepositoryIndicatorUpdater } from './helpers/repository-indicator-updater' import { isAttributableEmailFor } from '../email' import { TrashNameLabel } from '../../ui/lib/context-menu' @@ -6104,15 +6100,30 @@ export class AppStore extends TypedBaseStore { return } + const { parent, owner, name, htmlURL } = gitHubRepository + const isForkContributingToParent = + isRepositoryAForkContributingToParent(repository) + + const baseForkPreface = + isForkContributingToParent && parent !== null + ? `${parent.owner.login}:${parent.name}:` + : '' const encodedBaseBranch = baseBranch !== undefined - ? encodeURIComponent(baseBranch.nameWithoutRemote) + '...' + ? baseForkPreface + + encodeURIComponent(baseBranch.nameWithoutRemote) + + '...' : '' - const encodedCompareBranch = encodeURIComponent( - compareBranch.nameWithoutRemote - ) + + const compareForkPreface = isForkContributingToParent + ? `${owner.login}:${name}:` + : '' + + const encodedCompareBranch = + compareForkPreface + encodeURIComponent(compareBranch.nameWithoutRemote) + const compareString = `${encodedBaseBranch}${encodedCompareBranch}` - const baseURL = `${gitHubRepository.htmlURL}/pull/new/${compareString}` + const baseURL = `${htmlURL}/pull/new/${compareString}` await this._openInBrowser(baseURL) @@ -7410,11 +7421,9 @@ export class AppStore extends TypedBaseStore { * Other notes: A repo can only create a pull request if it is hosted on * dotcom. */ - const remote = - isRepositoryWithForkedGitHubRepository(repository) && - getForkContributionTarget(repository) === ForkContributionTarget.Parent - ? UpstreamRemoteName - : gitStore.defaultRemote?.name + const remote = isRepositoryAForkContributingToParent(repository) + ? UpstreamRemoteName + : gitStore.defaultRemote?.name const prBaseBranches = allBranches.filter( b => b.upstreamRemoteName === remote || b.remoteName === remote ) diff --git a/app/src/models/repository.ts b/app/src/models/repository.ts index e299d051bd..a8d06c5fa1 100644 --- a/app/src/models/repository.ts +++ b/app/src/models/repository.ts @@ -213,3 +213,15 @@ export function getForkContributionTarget( ? repository.workflowPreferences.forkContributionTarget : ForkContributionTarget.Parent } + +/** + * Returns whether the fork is contributing to the parent + */ +export function isRepositoryAForkContributingToParent( + repository: Repository +): boolean { + return ( + isRepositoryWithForkedGitHubRepository(repository) && + getForkContributionTarget(repository) === ForkContributionTarget.Parent + ) +} From bb340d87d43497eb1d6e19ddda5b95272cf62402 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Mon, 12 Dec 2022 13:18:44 -0500 Subject: [PATCH 238/262] Tidying.. --- app/src/lib/stores/app-store.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 88ca985783..74a4c866c9 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7417,9 +7417,6 @@ export class AppStore extends TypedBaseStore { branchesState /* 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. - * - * Other notes: A repo can only create a pull request if it is hosted on - * dotcom. */ const remote = isRepositoryAForkContributingToParent(repository) ? UpstreamRemoteName From 6c056236b99f7ef4306ddbdbe3e528c5bc9da1cc Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 13 Dec 2022 10:12:49 -0500 Subject: [PATCH 239/262] Send it as undefined to use default online --- app/src/ui/open-pull-request/open-pull-request-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx index 93935491ce..07fbf57837 100644 --- a/app/src/ui/open-pull-request/open-pull-request-dialog.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-dialog.tsx @@ -83,7 +83,7 @@ export class OpenPullRequestDialog extends React.Component Date: Tue, 13 Dec 2022 11:10:40 -0500 Subject: [PATCH 240/262] Revert unneeded change --- app/src/lib/stores/git-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/stores/git-store.ts b/app/src/lib/stores/git-store.ts index 0f776db9f1..84d367cea7 100644 --- a/app/src/lib/stores/git-store.ts +++ b/app/src/lib/stores/git-store.ts @@ -535,7 +535,7 @@ export class GitStore extends BaseStore { * using the available API data, remote information or branch * name conventions. */ - public async resolveDefaultBranch(): Promise { + private async resolveDefaultBranch(): Promise { if (this.currentRemote !== null) { // the Git server should use [remote]/HEAD to advertise // it's default branch, so see if it exists and matches From 9bacb57ebfc623b68f416724860067db525d806b Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 13 Dec 2022 12:06:44 -0500 Subject: [PATCH 241/262] Make isTopMostDialog function --- app/src/ui/about/about.tsx | 11 +++++----- .../ui/add-repository/create-repository.tsx | 10 +++++----- .../ui/clone-repository/clone-repository.tsx | 10 +++++----- app/src/ui/dialog/dialog.tsx | 10 +++++----- app/src/ui/dialog/is-top-most-service.tsx | 20 ------------------- app/src/ui/dialog/is-top-most.tsx | 14 +++++++++++++ 6 files changed, 35 insertions(+), 40 deletions(-) delete mode 100644 app/src/ui/dialog/is-top-most-service.tsx create mode 100644 app/src/ui/dialog/is-top-most.tsx diff --git a/app/src/ui/about/about.tsx b/app/src/ui/about/about.tsx index 5f6db61795..01d35115d2 100644 --- a/app/src/ui/about/about.tsx +++ b/app/src/ui/about/about.tsx @@ -16,7 +16,7 @@ import { RelativeTime } from '../relative-time' import { assertNever } from '../../lib/fatal-error' import { ReleaseNotesUri } from '../lib/releases' import { encodePathAsUrl } from '../../lib/path' -import { IsTopMostService } from '../dialog/is-top-most-service' +import { isTopMostDialog } from '../dialog/is-top-most' const logoPath = __DARWIN__ ? 'static/logo-64x64@2x.png' @@ -71,7 +71,7 @@ interface IAboutState { */ export class About extends React.Component { private updateStoreEventHandle: Disposable | null = null - private isTopMostService: IsTopMostService = new IsTopMostService( + private checkIsTopMostDialog = isTopMostDialog( () => { window.addEventListener('keydown', this.onKeyDown) window.addEventListener('keyup', this.onKeyUp) @@ -100,11 +100,11 @@ export class About extends React.Component { this.onUpdateStateChanged ) this.setState({ updateState: updateStore.state }) - this.isTopMostService.check(this.props.isTopMost) + this.checkIsTopMostDialog(this.props.isTopMost) } public componentDidUpdate(): void { - this.isTopMostService.check(this.props.isTopMost) + this.checkIsTopMostDialog(this.props.isTopMost) } public componentWillUnmount() { @@ -112,7 +112,8 @@ export class About extends React.Component { this.updateStoreEventHandle.dispose() this.updateStoreEventHandle = null } - this.isTopMostService.unmount() + window.removeEventListener('keydown', this.onKeyDown) + window.removeEventListener('keyup', this.onKeyUp) } private onKeyDown = (event: KeyboardEvent) => { diff --git a/app/src/ui/add-repository/create-repository.tsx b/app/src/ui/add-repository/create-repository.tsx index 9ff0369555..99e4fd1da4 100644 --- a/app/src/ui/add-repository/create-repository.tsx +++ b/app/src/ui/add-repository/create-repository.tsx @@ -36,7 +36,7 @@ import { mkdir } from 'fs/promises' import { directoryExists } from '../../lib/directory-exists' import { FoldoutType } from '../../lib/app-state' import { join } from 'path' -import { IsTopMostService } from '../dialog/is-top-most-service' +import { isTopMostDialog } from '../dialog/is-top-most' /** The sentinel value used to indicate no gitignore should be used. */ const NoGitIgnoreValue = 'None' @@ -119,7 +119,7 @@ export class CreateRepository extends React.Component< ICreateRepositoryProps, ICreateRepositoryState > { - private isTopMostService: IsTopMostService = new IsTopMostService( + private checkIsTopMostDialog = isTopMostDialog( () => { this.updateReadMeExists(this.state.path, this.state.name) window.addEventListener('focus', this.onWindowFocus) @@ -159,7 +159,7 @@ export class CreateRepository extends React.Component< } public async componentDidMount() { - this.isTopMostService.check(this.props.isTopMost) + this.checkIsTopMostDialog(this.props.isTopMost) const gitIgnoreNames = await getGitIgnoreNames() const licenses = await getLicenses() @@ -173,11 +173,11 @@ export class CreateRepository extends React.Component< } public componentDidUpdate(): void { - this.isTopMostService.check(this.props.isTopMost) + this.checkIsTopMostDialog(this.props.isTopMost) } public componentWillUnmount(): void { - this.isTopMostService.unmount() + window.removeEventListener('focus', this.onWindowFocus) } private initializePath = async () => { diff --git a/app/src/ui/clone-repository/clone-repository.tsx b/app/src/ui/clone-repository/clone-repository.tsx index 8ab4d4ee0e..c34678ee93 100644 --- a/app/src/ui/clone-repository/clone-repository.tsx +++ b/app/src/ui/clone-repository/clone-repository.tsx @@ -24,7 +24,7 @@ import { ClickSource } from '../lib/list' import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' import { showOpenDialog, showSaveDialog } from '../main-process-proxy' import { readdir } from 'fs/promises' -import { IsTopMostService } from '../dialog/is-top-most-service' +import { isTopMostDialog } from '../dialog/is-top-most' interface ICloneRepositoryProps { readonly dispatcher: Dispatcher @@ -152,7 +152,7 @@ export class CloneRepository extends React.Component< ICloneRepositoryProps, ICloneRepositoryState > { - private isTopMostService: IsTopMostService = new IsTopMostService( + private checkIsTopMostDialog = isTopMostDialog( () => { this.validatePath() window.addEventListener('focus', this.onWindowFocus) @@ -207,7 +207,7 @@ export class CloneRepository extends React.Component< this.updateUrl(this.props.initialURL || '') } - this.isTopMostService.check(this.props.isTopMost) + this.checkIsTopMostDialog(this.props.isTopMost) } public componentDidMount() { @@ -216,11 +216,11 @@ export class CloneRepository extends React.Component< this.updateUrl(initialURL) } - this.isTopMostService.check(this.props.isTopMost) + this.checkIsTopMostDialog(this.props.isTopMost) } public componentWillUnmount(): void { - this.isTopMostService.unmount() + window.removeEventListener('focus', this.onWindowFocus) } private initializePath = async () => { diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index b3c599e791..83be2c0195 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames' import { DialogHeader } from './header' import { createUniqueId, releaseUniqueId } from '../lib/id-pool' import { getTitleBarHeight } from '../window/title-bar' -import { IsTopMostService } from './is-top-most-service' +import { isTopMostDialog } from './is-top-most' export interface IDialogStackContext { /** Whether or not this dialog is the top most one in the stack to be @@ -167,7 +167,7 @@ export class Dialog extends React.Component { public static contextType = DialogStackContext public declare context: React.ContextType - private isTopMostService: IsTopMostService = new IsTopMostService( + private checkIsTopMostDialog = isTopMostDialog( () => { this.onDialogIsTopMost() }, @@ -287,7 +287,7 @@ export class Dialog extends React.Component { } public componentDidMount() { - this.isTopMostService.check(this.context.isTopMost) + this.checkIsTopMostDialog(this.context.isTopMost) } protected onDialogIsTopMost() { @@ -487,7 +487,7 @@ export class Dialog extends React.Component { releaseUniqueId(this.state.titleId) } - this.isTopMostService.unmount() + this.onDialogIsNotTopMost() } public componentDidUpdate(prevProps: IDialogProps) { @@ -495,7 +495,7 @@ export class Dialog extends React.Component { this.updateTitleId() } - this.isTopMostService.check(this.context.isTopMost) + this.checkIsTopMostDialog(this.context.isTopMost) } private onDialogCancel = (e: Event | React.SyntheticEvent) => { diff --git a/app/src/ui/dialog/is-top-most-service.tsx b/app/src/ui/dialog/is-top-most-service.tsx deleted file mode 100644 index 20d8aa5c7a..0000000000 --- a/app/src/ui/dialog/is-top-most-service.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import memoizeOne from 'memoize-one' - -export class IsTopMostService { - public check = memoizeOne((isTopMost: boolean) => { - if (isTopMost) { - this.onDialogIsTopMost() - } else { - this.onDialogIsNotTopMost() - } - }) - - public constructor( - private onDialogIsTopMost: () => void, - private onDialogIsNotTopMost: () => void - ) {} - - public unmount() { - this.onDialogIsNotTopMost() - } -} diff --git a/app/src/ui/dialog/is-top-most.tsx b/app/src/ui/dialog/is-top-most.tsx new file mode 100644 index 0000000000..3b28888d8a --- /dev/null +++ b/app/src/ui/dialog/is-top-most.tsx @@ -0,0 +1,14 @@ +import memoizeOne from 'memoize-one' + +export function isTopMostDialog( + onDialogIsTopMost: () => void, + onDialogIsNotTopMost: () => void +) { + return memoizeOne((isTopMost: boolean) => { + if (isTopMost) { + onDialogIsTopMost() + } else { + onDialogIsNotTopMost() + } + }) +} From 17229261546b33c8109759b32be5b969830b101e Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 13 Dec 2022 12:15:48 -0500 Subject: [PATCH 242/262] Tidying Co-Authored-By: Markus Olsson <634063+niik@users.noreply.github.com> --- app/src/ui/app.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index 255867cfba..1feefe8571 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -1415,12 +1415,7 @@ export class App extends React.Component { {allPopups.map(popup => { const isTopMost = this.state.currentPopup?.id === popup.id return ( - + {this.popupContent(popup, isTopMost)} ) From b819be49139b9db5cbbd7509c8c7fc3ec4247799 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Tue, 13 Dec 2022 12:38:25 -0500 Subject: [PATCH 243/262] Insert popups in place instead of constructing the array for sorting Co-Authored-By: Markus Olsson <634063+niik@users.noreply.github.com> --- app/src/lib/popup-manager.ts | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 20abd04c20..67d988ed8b 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -43,23 +43,23 @@ export class PopupManager { /** * 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.allPopups.at(-1) ?? null + return this.popupStack.at(-1) ?? null } /** - * Returns all the popups in the stack. If there are error popups, it returns - * them on the top of the stack (the end of the array -> last on, last off). + * 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 { - const errorPopups = this.getPopupsOfType(PopupType.Error) - const nonErrorPopups = this.popupStack.filter( - p => p.type !== PopupType.Error - ) - return [...nonErrorPopups, ...errorPopups] + return this.popupStack } /** @@ -109,11 +109,25 @@ export class PopupManager { return popupToAdd } - this.popupStack.push(popup) + this.insertBeforeErrorPopups(popup) this.checkStackLength() return popup } + /** Adds a non-Error type popup before any error popups. */ + private insertBeforeErrorPopups(popup: Popup) { + const indexLastError = this.popupStack.findIndex( + p => p.type === PopupType.Error + ) + + if (indexLastError === -1) { + this.popupStack.push(popup) + return + } + + this.popupStack.splice(indexLastError, 0, popup) + } + /* * Adds an Error Popup to the stack * - The popup will be given a unique id. From 3b722a6776c0c8b88d6d3f21f537539fe23fc473 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 14 Dec 2022 08:24:13 -0500 Subject: [PATCH 244/262] Redefine array on insert Co-authored-by: Markus Olsson --- app/src/lib/popup-manager.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index 67d988ed8b..f56f6f1cb9 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -116,16 +116,16 @@ export class PopupManager { /** Adds a non-Error type popup before any error popups. */ private insertBeforeErrorPopups(popup: Popup) { - const indexLastError = this.popupStack.findIndex( - p => p.type === PopupType.Error - ) - - if (indexLastError === -1) { - this.popupStack.push(popup) + if (this.popupStack.at(-1)?.type !== PopupType.Error) { + this.popupStack = this.popupStack.concat(popup) return } - this.popupStack.splice(indexLastError, 0, popup) + const errorPopups = this.getPopupsOfType(PopupType.Error) + const nonErrorPopups = this.popupStack.filter( + p => p.type !== PopupType.Error + ) + this.popupStack = [...nonErrorPopups, popup, ...errorPopups] } /* From 7d65115d0553e78fd4019806aa24e6b2e233caf0 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 14 Dec 2022 08:35:34 -0500 Subject: [PATCH 245/262] Doc Co-Authored-By: Markus Olsson <634063+niik@users.noreply.github.com> --- app/src/ui/dialog/is-top-most.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/ui/dialog/is-top-most.tsx b/app/src/ui/dialog/is-top-most.tsx index 3b28888d8a..c6247d817a 100644 --- a/app/src/ui/dialog/is-top-most.tsx +++ b/app/src/ui/dialog/is-top-most.tsx @@ -1,5 +1,8 @@ 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 From d2e001530570ec46eda4d649be0916a86774cefa Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 14 Dec 2022 08:41:30 -0500 Subject: [PATCH 246/262] Make popupStack Readonly Co-Authored-By: Markus Olsson <634063+niik@users.noreply.github.com> --- app/src/lib/popup-manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/lib/popup-manager.ts b/app/src/lib/popup-manager.ts index f56f6f1cb9..6afb2449a6 100644 --- a/app/src/lib/popup-manager.ts +++ b/app/src/lib/popup-manager.ts @@ -37,7 +37,7 @@ const defaultPopupStackLimit = 50 * displayed as error text with a ok button. */ export class PopupManager { - private popupStack = new Array() + private popupStack: ReadonlyArray = [] public constructor(private readonly popupLimit = defaultPopupStackLimit) {} @@ -135,7 +135,7 @@ export class PopupManager { **/ public addErrorPopup(error: Error): Popup { const popup: Popup = { id: uuid(), type: PopupType.Error, error } - this.popupStack.push(popup) + this.popupStack = this.popupStack.concat(popup) this.checkStackLength() return popup } From 23aec14b7552b9c0b3a438697c9209ba81526cb2 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 14 Dec 2022 08:54:19 -0500 Subject: [PATCH 247/262] Use helper Co-authored-by: Markus Olsson --- app/src/ui/clone-repository/clone-repository.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/clone-repository/clone-repository.tsx b/app/src/ui/clone-repository/clone-repository.tsx index c34678ee93..dc70a90970 100644 --- a/app/src/ui/clone-repository/clone-repository.tsx +++ b/app/src/ui/clone-repository/clone-repository.tsx @@ -220,7 +220,7 @@ export class CloneRepository extends React.Component< } public componentWillUnmount(): void { - window.removeEventListener('focus', this.onWindowFocus) + this.checkIsTopMostDialog(false) } private initializePath = async () => { From 84fb5980084d935643257a4a1c7057e543ee4ba2 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 14 Dec 2022 08:54:26 -0500 Subject: [PATCH 248/262] Use helper Co-authored-by: Markus Olsson --- app/src/ui/add-repository/create-repository.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/add-repository/create-repository.tsx b/app/src/ui/add-repository/create-repository.tsx index 99e4fd1da4..0d9ce9e80c 100644 --- a/app/src/ui/add-repository/create-repository.tsx +++ b/app/src/ui/add-repository/create-repository.tsx @@ -177,7 +177,7 @@ export class CreateRepository extends React.Component< } public componentWillUnmount(): void { - window.removeEventListener('focus', this.onWindowFocus) + this.checkIsTopMostDialog(false) } private initializePath = async () => { From ef7c99832740e6983db16f34cc6b5cb5a1d69452 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 14 Dec 2022 08:57:51 -0500 Subject: [PATCH 249/262] Other isTopMost unmounts consistent Co-Authored-By: Markus Olsson <634063+niik@users.noreply.github.com> --- app/src/ui/about/about.tsx | 3 +-- app/src/ui/dialog/dialog.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/ui/about/about.tsx b/app/src/ui/about/about.tsx index 01d35115d2..ad5ecf5de2 100644 --- a/app/src/ui/about/about.tsx +++ b/app/src/ui/about/about.tsx @@ -112,8 +112,7 @@ export class About extends React.Component { this.updateStoreEventHandle.dispose() this.updateStoreEventHandle = null } - window.removeEventListener('keydown', this.onKeyDown) - window.removeEventListener('keyup', this.onKeyUp) + this.checkIsTopMostDialog(false) } private onKeyDown = (event: KeyboardEvent) => { diff --git a/app/src/ui/dialog/dialog.tsx b/app/src/ui/dialog/dialog.tsx index 83be2c0195..ff7b39598d 100644 --- a/app/src/ui/dialog/dialog.tsx +++ b/app/src/ui/dialog/dialog.tsx @@ -487,7 +487,7 @@ export class Dialog extends React.Component { releaseUniqueId(this.state.titleId) } - this.onDialogIsNotTopMost() + this.checkIsTopMostDialog(false) } public componentDidUpdate(prevProps: IDialogProps) { From 695d121c2c310f3b6c45fe13a2d15727395c9b55 Mon Sep 17 00:00:00 2001 From: tidy-dev Date: Wed, 14 Dec 2022 11:10:16 -0500 Subject: [PATCH 250/262] Release 3.1.3-beta4 --- app/package.json | 2 +- changelog.json | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index b5ea4b262c..49c3194324 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "productName": "GitHub Desktop", "bundleID": "com.github.GitHubClient", "companyName": "GitHub, Inc.", - "version": "3.1.3-beta3", + "version": "3.1.3-beta4", "main": "./main.js", "repository": { "type": "git", diff --git a/changelog.json b/changelog.json index 4c3a01ab37..fecd1afd51 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,17 @@ { "releases": { + "3.1.3-beta4": [ + "[Fixed] Hide window instead of hide the app in 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", From 95a9cd37fb308781cf46ab57c908f8fa776aca5d Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Thu, 15 Dec 2022 07:52:47 -0500 Subject: [PATCH 251/262] Update changelog.json Co-authored-by: Sergio Padrino --- changelog.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.json b/changelog.json index fecd1afd51..ba2bb858cf 100644 --- a/changelog.json +++ b/changelog.json @@ -1,7 +1,7 @@ { "releases": { "3.1.3-beta4": [ - "[Fixed] Hide window instead of hide the app in macOS - #15511. Thanks @angusdev!", + "[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", From 0d2335948bc28f64608b22864e69339209a777b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 16:28:57 +0000 Subject: [PATCH 252/262] Bump json5 from 1.0.1 to 1.0.2 Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index ffa6ec57cc..fa2dcf36f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6410,9 +6410,9 @@ json5@2.x, json5@^2.1.2: minimist "^1.2.5" json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -7057,26 +7057,11 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -minimist@^1.2.0: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -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: version "1.2.0" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.2.0.tgz#d02b8c6f8b6d4b8f5982d3fd009c4919851c3fe2" From 7165ffe67297db63180890cb595ed51b573b72b8 Mon Sep 17 00:00:00 2001 From: zipperer <47086307+zipperer@users.noreply.github.com> Date: Tue, 3 Jan 2023 11:25:11 -0600 Subject: [PATCH 253/262] Update linux.ts -- add editor Emacs In `editors: ILinuxExternalEditor[]`, add entry for [Emacs](https://www.gnu.org/savannah-checkouts/gnu/emacs/emacs.html). --- app/src/lib/editors/linux.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/lib/editors/linux.ts b/app/src/lib/editors/linux.ts index de7953eb3d..0670548b44 100644 --- a/app/src/lib/editors/linux.ts +++ b/app/src/lib/editors/linux.ts @@ -70,6 +70,10 @@ const editors: ILinuxExternalEditor[] = [ 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 { From 061bf74553e594a6be21b0303069b2e7aa7a200a Mon Sep 17 00:00:00 2001 From: zipperer <47086307+zipperer@users.noreply.github.com> Date: Tue, 3 Jan 2023 11:46:08 -0600 Subject: [PATCH 254/262] Update editor-integration.md Linux editors list -- add entry for Emacs. --- docs/technical/editor-integration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/technical/editor-integration.md b/docs/technical/editor-integration.md index 095c4cbc08..aa12a0ab19 100644 --- a/docs/technical/editor-integration.md +++ b/docs/technical/editor-integration.md @@ -307,6 +307,7 @@ These editors are currently supported: - [Neovim](https://neovim.io/) - [Code](https://github.com/elementary/code) - [Lite XL](https://lite-xl.com/) + - [Emacs](https://www.gnu.org/software/emacs/) These are defined in a list at the top of the file: From 6e601a551ec52eb077b4732545aa91f258cf4241 Mon Sep 17 00:00:00 2001 From: zipperer <47086307+zipperer@users.noreply.github.com> Date: Tue, 3 Jan 2023 11:53:07 -0600 Subject: [PATCH 255/262] Update editor-integration.md Linux editors list: add entries for JetBrains {PHPStorm|WebStorm}. Before this commit, [`app/src/lib/editors/linux.ts`](https://github.com/desktop/desktop/blob/development/app/src/lib/editors/linux.ts) includes these editors. This commit is not related to the pull request for adding Emacs. However, I tacked this commit onto the pull request in passing. --- docs/technical/editor-integration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/technical/editor-integration.md b/docs/technical/editor-integration.md index aa12a0ab19..b0a0480d68 100644 --- a/docs/technical/editor-integration.md +++ b/docs/technical/editor-integration.md @@ -307,6 +307,8 @@ These editors are currently supported: - [Neovim](https://neovim.io/) - [Code](https://github.com/elementary/code) - [Lite XL](https://lite-xl.com/) + - [JetBrains PHPStorm](https://www.jetbrains.com/phpstorm/) + - [JetBrains WebStorm](https://www.jetbrains.com/webstorm/) - [Emacs](https://www.gnu.org/software/emacs/) These are defined in a list at the top of the file: From 0e3cc645dd7773ff540abacf343046a38390cf20 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 4 Jan 2023 08:22:01 -0500 Subject: [PATCH 256/262] Better naming Co-Authored-By: Sergio Padrino <1083228+sergiou87@users.noreply.github.com> --- app/src/lib/stores/app-store.ts | 6 +++--- app/src/models/repository.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 74a4c866c9..9f0c2aea21 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -47,7 +47,7 @@ import { isRepositoryWithGitHubRepository, RepositoryWithGitHubRepository, getNonForkGitHubRepository, - isRepositoryAForkContributingToParent, + isForkedRepositoryContributingToParent, } from '../../models/repository' import { CommittedFileChange, @@ -6102,7 +6102,7 @@ export class AppStore extends TypedBaseStore { const { parent, owner, name, htmlURL } = gitHubRepository const isForkContributingToParent = - isRepositoryAForkContributingToParent(repository) + isForkedRepositoryContributingToParent(repository) const baseForkPreface = isForkContributingToParent && parent !== null @@ -7418,7 +7418,7 @@ export class AppStore extends TypedBaseStore { /* 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 = isRepositoryAForkContributingToParent(repository) + const remote = isForkedRepositoryContributingToParent(repository) ? UpstreamRemoteName : gitStore.defaultRemote?.name const prBaseBranches = allBranches.filter( diff --git a/app/src/models/repository.ts b/app/src/models/repository.ts index a8d06c5fa1..74b9a89b9e 100644 --- a/app/src/models/repository.ts +++ b/app/src/models/repository.ts @@ -217,7 +217,7 @@ export function getForkContributionTarget( /** * Returns whether the fork is contributing to the parent */ -export function isRepositoryAForkContributingToParent( +export function isForkedRepositoryContributingToParent( repository: Repository ): boolean { return ( From ecc5b00ec3c2754ae07d910e4409759f664365ab Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 4 Jan 2023 08:29:30 -0500 Subject: [PATCH 257/262] Lint.. --- app/src/lib/stores/app-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index 9f0c2aea21..a6ee5d0bc1 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -6102,7 +6102,7 @@ export class AppStore extends TypedBaseStore { const { parent, owner, name, htmlURL } = gitHubRepository const isForkContributingToParent = - isForkedRepositoryContributingToParent(repository) + isForkedRepositoryContributingToParent(repository) const baseForkPreface = isForkContributingToParent && parent !== null From fc1acbf6d5bb09d2889b91fc643b3ac578181537 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 4 Jan 2023 09:24:17 -0500 Subject: [PATCH 258/262] Better messaging when no branch found Co-Authored-By: Sergio Padrino <1083228+sergiou87@users.noreply.github.com> --- app/src/ui/branches/branch-list.tsx | 4 ++++ app/src/ui/branches/branch-select.tsx | 13 +++++++++++-- app/src/ui/branches/no-branches.tsx | 8 +++++++- .../open-pull-request/open-pull-request-header.tsx | 6 ++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/src/ui/branches/branch-list.tsx b/app/src/ui/branches/branch-list.tsx index a3db3b365c..8240d6f292 100644 --- a/app/src/ui/branches/branch-list.tsx +++ b/app/src/ui/branches/branch-list.tsx @@ -110,6 +110,9 @@ interface IBranchListProps { /** Called to render content before/above the branches filter and list. */ readonly renderPreList?: () => JSX.Element | null + + /** Optional: No branches message */ + readonly noBranchesMessage?: string | JSX.Element } interface IBranchListState { @@ -249,6 +252,7 @@ export class BranchList extends React.Component< ) } diff --git a/app/src/ui/branches/branch-select.tsx b/app/src/ui/branches/branch-select.tsx index e79b5c6742..b7267c3038 100644 --- a/app/src/ui/branches/branch-select.tsx +++ b/app/src/ui/branches/branch-select.tsx @@ -33,6 +33,9 @@ interface IBranchSelectProps { /** Called when the user changes the selected branch. */ readonly onChange?: (branch: Branch) => void + + /** Optional: No branches message */ + readonly noBranchesMessage?: string | JSX.Element } interface IBranchSelectState { @@ -74,8 +77,13 @@ export class BranchSelect extends React.Component< } public render() { - const { currentBranch, defaultBranch, recentBranches, allBranches } = - this.props + const { + currentBranch, + defaultBranch, + recentBranches, + allBranches, + noBranchesMessage, + } = this.props const { filterText, selectedBranch } = this.state @@ -97,6 +105,7 @@ export class BranchSelect extends React.Component< canCreateNewBranch={false} renderBranch={this.renderBranch} onItemClick={this.onItemClick} + noBranchesMessage={noBranchesMessage} /> ) diff --git a/app/src/ui/branches/no-branches.tsx b/app/src/ui/branches/no-branches.tsx index da0a736d02..d2c269e4f0 100644 --- a/app/src/ui/branches/no-branches.tsx +++ b/app/src/ui/branches/no-branches.tsx @@ -12,6 +12,8 @@ interface INoBranchesProps { readonly onCreateNewBranch: () => void /** True to display the UI elements for creating a new branch, false to hide them */ readonly canCreateNewBranch: boolean + /** Optional: No branches message */ + readonly noBranchesMessage?: string | JSX.Element } export class NoBranches extends React.Component { @@ -43,7 +45,11 @@ export class NoBranches extends React.Component { ) } - return
Sorry, I can't find that branch
+ return ( +
+ {this.props.noBranchesMessage ?? "Sorry, I can't find that branch"} +
+ ) } private renderShortcut() { diff --git a/app/src/ui/open-pull-request/open-pull-request-header.tsx b/app/src/ui/open-pull-request/open-pull-request-header.tsx index 10d008974d..e236821053 100644 --- a/app/src/ui/open-pull-request/open-pull-request-header.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-header.tsx @@ -104,6 +104,12 @@ export class OpenPullRequestDialogHeader extends React.Component< allBranches={prBaseBranches} recentBranches={prRecentBaseBranches} onChange={onBranchChange} + noBranchesMessage={ + <> + Sorry, I can't find a that remote branch.
+ You can only open pull requests against remote branches. + + } />{' '} from {currentBranch.name}.
From ce10ec491f229513117a8820b548baf710847d42 Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 4 Jan 2023 09:36:45 -0500 Subject: [PATCH 259/262] Some reason this was missing? --- app/src/lib/stores/app-store.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index d0d22f07a6..6a2e8b5260 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -7449,6 +7449,7 @@ export class AppStore extends TypedBaseStore { 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. */ From 37d0a591353d07795b09569ab7a6fa71e573990a Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 4 Jan 2023 09:38:42 -0500 Subject: [PATCH 260/262] typo.. Co-authored-by: Sergio Padrino --- app/src/ui/open-pull-request/open-pull-request-header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ui/open-pull-request/open-pull-request-header.tsx b/app/src/ui/open-pull-request/open-pull-request-header.tsx index 89556bbe23..5b0b30273c 100644 --- a/app/src/ui/open-pull-request/open-pull-request-header.tsx +++ b/app/src/ui/open-pull-request/open-pull-request-header.tsx @@ -106,7 +106,7 @@ export class OpenPullRequestDialogHeader extends React.Component< onChange={onBranchChange} noBranchesMessage={ <> - Sorry, I can't find a that remote branch.
+ Sorry, I can't find that remote branch.
You can only open pull requests against remote branches. } From 2b3eaaaffd9792d3c63f09a997317d3eebc58eca Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 4 Jan 2023 10:30:26 -0500 Subject: [PATCH 261/262] Align no branches text in the center --- app/styles/ui/_no-branches.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/ui/_no-branches.scss b/app/styles/ui/_no-branches.scss index 6b5dc0fcfd..a33e3c7fdb 100644 --- a/app/styles/ui/_no-branches.scss +++ b/app/styles/ui/_no-branches.scss @@ -3,6 +3,7 @@ flex: 1; flex-direction: column; align-items: center; + text-align: center; padding: var(--spacing); From af7b8ae7d1f88ba93e01731cdd7d070ee7ca4a7e Mon Sep 17 00:00:00 2001 From: tidy-dev <75402236+tidy-dev@users.noreply.github.com> Date: Wed, 4 Jan 2023 10:31:21 -0500 Subject: [PATCH 262/262] Revert "Align no branches text in the center" This reverts commit 2b3eaaaffd9792d3c63f09a997317d3eebc58eca. --- app/styles/ui/_no-branches.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/styles/ui/_no-branches.scss b/app/styles/ui/_no-branches.scss index a33e3c7fdb..6b5dc0fcfd 100644 --- a/app/styles/ui/_no-branches.scss +++ b/app/styles/ui/_no-branches.scss @@ -3,7 +3,6 @@ flex: 1; flex-direction: column; align-items: center; - text-align: center; padding: var(--spacing);