mirror of
https://github.com/desktop/desktop
synced 2024-08-28 20:51:16 +00:00
Merge branch 'development' into releases/3.1.7-beta1
This commit is contained in:
commit
ccddcaa46e
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -32,19 +32,7 @@ jobs:
|
|||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
${{ runner.os }}-yarn-
|
||||
cache: yarn
|
||||
|
||||
# This step can be removed as soon as official Windows arm64 builds are published:
|
||||
# https://github.com/nodejs/build/issues/2450#issuecomment-705853342
|
||||
|
|
|
@ -508,6 +508,15 @@ export interface IAPIPullRequestReview {
|
|||
| 'CHANGES_REQUESTED'
|
||||
}
|
||||
|
||||
/** Represents both issue comments and PR review comments */
|
||||
export interface IAPIComment {
|
||||
readonly id: number
|
||||
readonly body: string
|
||||
readonly html_url: string
|
||||
readonly user: IAPIIdentity
|
||||
readonly created_at: string
|
||||
}
|
||||
|
||||
/** The metadata about a GitHub server. */
|
||||
export interface IServerMetadata {
|
||||
/**
|
||||
|
@ -1047,6 +1056,64 @@ export class API {
|
|||
}
|
||||
}
|
||||
|
||||
/** Fetches all reviews from a given pull request. */
|
||||
public async fetchPullRequestReviews(
|
||||
owner: string,
|
||||
name: string,
|
||||
prNumber: string
|
||||
) {
|
||||
try {
|
||||
const path = `/repos/${owner}/${name}/pulls/${prNumber}/reviews`
|
||||
const response = await this.request('GET', path)
|
||||
return await parsedResponse<IAPIPullRequestReview[]>(response)
|
||||
} catch (e) {
|
||||
log.debug(
|
||||
`failed fetching PR reviews for ${owner}/${name}/pulls/${prNumber}`,
|
||||
e
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetches all review comments from a given pull request. */
|
||||
public async fetchPullRequestReviewComments(
|
||||
owner: string,
|
||||
name: string,
|
||||
prNumber: string,
|
||||
reviewId: string
|
||||
) {
|
||||
try {
|
||||
const path = `/repos/${owner}/${name}/pulls/${prNumber}/reviews/${reviewId}/comments`
|
||||
const response = await this.request('GET', path)
|
||||
return await parsedResponse<IAPIComment[]>(response)
|
||||
} catch (e) {
|
||||
log.debug(
|
||||
`failed fetching PR review comments for ${owner}/${name}/pulls/${prNumber}`,
|
||||
e
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetches all comments from a given pull request. */
|
||||
public async fetchPullRequestComments(
|
||||
owner: string,
|
||||
name: string,
|
||||
prNumber: string
|
||||
) {
|
||||
try {
|
||||
const path = `/repos/${owner}/${name}/pulls/${prNumber}/comments`
|
||||
const response = await this.request('GET', path)
|
||||
return await parsedResponse<IAPIComment[]>(response)
|
||||
} catch (e) {
|
||||
log.debug(
|
||||
`failed fetching PR comments for ${owner}/${name}/pulls/${prNumber}`,
|
||||
e
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the combined status for the given ref.
|
||||
*/
|
||||
|
|
|
@ -71,6 +71,10 @@ const editors: IDarwinExternalEditor[] = [
|
|||
name: 'PyCharm Community Edition',
|
||||
bundleIdentifiers: ['com.jetbrains.pycharm.ce'],
|
||||
},
|
||||
{
|
||||
name: 'DataSpell',
|
||||
bundleIdentifiers: ['com.jetbrains.DataSpell'],
|
||||
},
|
||||
{
|
||||
name: 'RubyMine',
|
||||
bundleIdentifiers: ['com.jetbrains.RubyMine'],
|
||||
|
|
|
@ -23,9 +23,26 @@ const editors: ILinuxExternalEditor[] = [
|
|||
name: 'Neovim',
|
||||
paths: ['/usr/bin/nvim'],
|
||||
},
|
||||
{
|
||||
name: 'Neovim-Qt',
|
||||
paths: ['/usr/bin/nvim-qt'],
|
||||
},
|
||||
{
|
||||
name: 'Neovide',
|
||||
paths: ['/usr/bin/neovide'],
|
||||
},
|
||||
{
|
||||
name: 'gVim',
|
||||
paths: ['/usr/bin/gvim'],
|
||||
},
|
||||
{
|
||||
name: 'Visual Studio Code',
|
||||
paths: ['/usr/share/code/bin/code', '/snap/bin/code', '/usr/bin/code'],
|
||||
paths: [
|
||||
'/usr/share/code/bin/code',
|
||||
'/snap/bin/code',
|
||||
'/usr/bin/code',
|
||||
'/mnt/c/Program Files/Microsoft VS Code/bin/code',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Visual Studio Code (Insiders)',
|
||||
|
@ -33,7 +50,11 @@ const editors: ILinuxExternalEditor[] = [
|
|||
},
|
||||
{
|
||||
name: 'VSCodium',
|
||||
paths: ['/usr/bin/codium', '/var/lib/flatpak/app/com.vscodium.codium'],
|
||||
paths: [
|
||||
'/usr/bin/codium',
|
||||
'/var/lib/flatpak/app/com.vscodium.codium',
|
||||
'/usr/share/vscodium-bin/bin/codium',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Sublime Text',
|
||||
|
@ -63,17 +84,69 @@ const editors: ILinuxExternalEditor[] = [
|
|||
paths: ['/usr/bin/lite-xl'],
|
||||
},
|
||||
{
|
||||
name: 'Jetbrains PhpStorm',
|
||||
paths: ['/snap/bin/phpstorm'],
|
||||
name: 'JetBrains PhpStorm',
|
||||
paths: [
|
||||
'/snap/bin/phpstorm',
|
||||
'.local/share/JetBrains/Toolbox/scripts/phpstorm',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Jetbrains WebStorm',
|
||||
paths: ['/snap/bin/webstorm'],
|
||||
name: 'JetBrains WebStorm',
|
||||
paths: [
|
||||
'/snap/bin/webstorm',
|
||||
'.local/share/JetBrains/Toolbox/scripts/webstorm',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'IntelliJ IDEA',
|
||||
paths: ['/snap/bin/idea', '.local/share/JetBrains/Toolbox/scripts/idea'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains PyCharm',
|
||||
paths: [
|
||||
'/snap/bin/pycharm',
|
||||
'.local/share/JetBrains/Toolbox/scripts/pycharm',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Android Studio',
|
||||
paths: [
|
||||
'/snap/bin/studio',
|
||||
'.local/share/JetBrains/Toolbox/scripts/studio',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Emacs',
|
||||
paths: ['/snap/bin/emacs', '/usr/local/bin/emacs', '/usr/bin/emacs'],
|
||||
},
|
||||
{
|
||||
name: 'Kate',
|
||||
paths: ['/usr/bin/kate'],
|
||||
},
|
||||
{
|
||||
name: 'GEdit',
|
||||
paths: ['/usr/bin/gedit'],
|
||||
},
|
||||
{
|
||||
name: 'GNOME Text Editor',
|
||||
paths: ['/usr/bin/gnome-text-editor'],
|
||||
},
|
||||
{
|
||||
name: 'GNOME Builder',
|
||||
paths: ['/usr/bin/gnome-builder'],
|
||||
},
|
||||
{
|
||||
name: 'Notepadqq',
|
||||
paths: ['/usr/bin/notepadqq'],
|
||||
},
|
||||
{
|
||||
name: 'Geany',
|
||||
paths: ['/usr/bin/geany'],
|
||||
},
|
||||
{
|
||||
name: 'Mousepad',
|
||||
paths: ['/usr/bin/mousepad'],
|
||||
},
|
||||
]
|
||||
|
||||
async function getAvailablePath(paths: string[]): Promise<string | null> {
|
||||
|
|
|
@ -471,6 +471,14 @@ const editors: WindowsExternalEditor[] = [
|
|||
displayNamePrefix: 'Fleet ',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
{
|
||||
name: 'JetBrains DataSpell',
|
||||
registryKeys: registryKeysForJetBrainsIDE('DataSpell'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('dataspell'),
|
||||
jetBrainsToolboxScriptName: 'dataspell',
|
||||
displayNamePrefix: 'DataSpell ',
|
||||
publishers: ['JetBrains s.r.o.'],
|
||||
},
|
||||
]
|
||||
|
||||
function getKeyOrEmpty(
|
||||
|
|
|
@ -28,13 +28,23 @@ export interface IDesktopPullRequestReviewSubmitAliveEvent {
|
|||
readonly pull_request_number: number
|
||||
readonly state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED'
|
||||
readonly review_id: string
|
||||
readonly number_of_comments: number
|
||||
}
|
||||
|
||||
export interface IDesktopPullRequestCommentAliveEvent {
|
||||
readonly type: 'pr-comment'
|
||||
readonly subtype: 'review-comment' | 'issue-comment'
|
||||
readonly timestamp: number
|
||||
readonly owner: string
|
||||
readonly repo: string
|
||||
readonly pull_request_number: number
|
||||
readonly comment_id: string
|
||||
}
|
||||
|
||||
/** Represents an Alive event relevant to Desktop. */
|
||||
export type DesktopAliveEvent =
|
||||
| IDesktopChecksFailedAliveEvent
|
||||
| IDesktopPullRequestReviewSubmitAliveEvent
|
||||
| IDesktopPullRequestCommentAliveEvent
|
||||
interface IAliveSubscription {
|
||||
readonly account: Account
|
||||
readonly subscription: Subscription<AliveStore>
|
||||
|
@ -247,7 +257,11 @@ export class AliveStore {
|
|||
}
|
||||
|
||||
const data = event.data as any as DesktopAliveEvent
|
||||
if (data.type === 'pr-checks-failed' || data.type === 'pr-review-submit') {
|
||||
if (
|
||||
data.type === 'pr-checks-failed' ||
|
||||
data.type === 'pr-review-submit' ||
|
||||
data.type === 'pr-comment'
|
||||
) {
|
||||
this.emitter.emit(this.ALIVE_EVENT_RECEIVED_EVENT, data)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7300,8 +7300,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
private onPullRequestReviewSubmitNotification = async (
|
||||
repository: RepositoryWithGitHubRepository,
|
||||
pullRequest: PullRequest,
|
||||
review: ValidNotificationPullRequestReview,
|
||||
numberOfComments: number
|
||||
review: ValidNotificationPullRequestReview
|
||||
) => {
|
||||
const selectedRepository =
|
||||
this.selectedRepository ?? (await this._selectRepository(repository))
|
||||
|
@ -7322,7 +7321,6 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
review,
|
||||
pullRequest,
|
||||
repository,
|
||||
numberOfComments,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
151
app/src/lib/stores/notifications-debug-store.ts
Normal file
151
app/src/lib/stores/notifications-debug-store.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
import { GitHubRepository } from '../../models/github-repository'
|
||||
import { PullRequest } from '../../models/pull-request'
|
||||
import { RepositoryWithGitHubRepository } from '../../models/repository'
|
||||
import { API, IAPIComment } from '../api'
|
||||
import {
|
||||
isValidNotificationPullRequestReview,
|
||||
ValidNotificationPullRequestReview,
|
||||
} from '../valid-notification-pull-request-review'
|
||||
import { AccountsStore } from './accounts-store'
|
||||
import { NotificationsStore } from './notifications-store'
|
||||
import { PullRequestCoordinator } from './pull-request-coordinator'
|
||||
|
||||
/**
|
||||
* This class allows the TestNotifications dialog to fetch real data to simulate
|
||||
* notifications.
|
||||
*/
|
||||
export class NotificationsDebugStore {
|
||||
public constructor(
|
||||
private readonly accountsStore: AccountsStore,
|
||||
private readonly notificationsStore: NotificationsStore,
|
||||
private readonly pullRequestCoordinator: PullRequestCoordinator
|
||||
) {}
|
||||
|
||||
private async getAccountForRepository(repository: GitHubRepository) {
|
||||
const { endpoint } = repository
|
||||
|
||||
const accounts = await this.accountsStore.getAll()
|
||||
return accounts.find(a => a.endpoint === endpoint) ?? null
|
||||
}
|
||||
|
||||
private async getAPIForRepository(repository: GitHubRepository) {
|
||||
const account = await this.getAccountForRepository(repository)
|
||||
|
||||
if (account === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return API.fromAccount(account)
|
||||
}
|
||||
|
||||
/** Fetch all pull requests for the given repository. */
|
||||
public async getPullRequests(repository: RepositoryWithGitHubRepository) {
|
||||
return this.pullRequestCoordinator.getAllPullRequests(repository)
|
||||
}
|
||||
|
||||
/** Fetch all reviews for the given pull request. */
|
||||
public async getPullRequestReviews(
|
||||
repository: RepositoryWithGitHubRepository,
|
||||
pullRequestNumber: number
|
||||
) {
|
||||
const api = await this.getAPIForRepository(repository.gitHubRepository)
|
||||
if (api === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
const ghRepository = repository.gitHubRepository
|
||||
|
||||
const reviews = await api.fetchPullRequestReviews(
|
||||
ghRepository.owner.login,
|
||||
ghRepository.name,
|
||||
pullRequestNumber.toString()
|
||||
)
|
||||
|
||||
return reviews.filter(isValidNotificationPullRequestReview)
|
||||
}
|
||||
|
||||
/** Fetch all comments (issue and review comments) for the given pull request. */
|
||||
public async getPullRequestComments(
|
||||
repository: RepositoryWithGitHubRepository,
|
||||
pullRequestNumber: number
|
||||
) {
|
||||
const api = await this.getAPIForRepository(repository.gitHubRepository)
|
||||
if (api === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
const ghRepository = repository.gitHubRepository
|
||||
|
||||
const issueComments = await api.fetchPullRequestComments(
|
||||
ghRepository.owner.login,
|
||||
ghRepository.name,
|
||||
pullRequestNumber.toString()
|
||||
)
|
||||
|
||||
const issueCommentIds = new Set(issueComments.map(c => c.id))
|
||||
|
||||
// Fetch review comments of type COMMENTED and with no body
|
||||
const allReviews = await api.fetchPullRequestReviews(
|
||||
ghRepository.owner.login,
|
||||
ghRepository.name,
|
||||
pullRequestNumber.toString()
|
||||
)
|
||||
|
||||
const commentedReviewsWithNoBody = allReviews.filter(
|
||||
review => review.state === 'COMMENTED' && !review.body
|
||||
)
|
||||
|
||||
const allReviewComments = await Promise.all(
|
||||
commentedReviewsWithNoBody.map(review =>
|
||||
api.fetchPullRequestReviewComments(
|
||||
ghRepository.owner.login,
|
||||
ghRepository.name,
|
||||
pullRequestNumber.toString(),
|
||||
review.id.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Only reviews with one comment, and that comment is not an issue comment
|
||||
const singleReviewComments = allReviewComments
|
||||
.flatMap(comments => (comments.length === 1 ? comments : []))
|
||||
.filter(comment => !issueCommentIds.has(comment.id))
|
||||
|
||||
return [...issueComments, ...singleReviewComments]
|
||||
}
|
||||
|
||||
/** Simulate a notification for the given pull request review. */
|
||||
public simulatePullRequestReviewNotification(
|
||||
repository: GitHubRepository,
|
||||
pullRequest: PullRequest,
|
||||
review: ValidNotificationPullRequestReview
|
||||
) {
|
||||
this.notificationsStore.simulateAliveEvent({
|
||||
type: 'pr-review-submit',
|
||||
timestamp: new Date(review.submitted_at).getTime(),
|
||||
owner: repository.owner.login,
|
||||
repo: repository.name,
|
||||
pull_request_number: pullRequest.pullRequestNumber,
|
||||
state: review.state,
|
||||
review_id: review.id.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
/** Simulate a notification for the given pull request comment. */
|
||||
public simulatePullRequestCommentNotification(
|
||||
repository: GitHubRepository,
|
||||
pullRequest: PullRequest,
|
||||
comment: IAPIComment,
|
||||
isIssueComment: boolean
|
||||
) {
|
||||
this.notificationsStore.simulateAliveEvent({
|
||||
type: 'pr-comment',
|
||||
subtype: isIssueComment ? 'issue-comment' : 'review-comment',
|
||||
timestamp: new Date(comment.created_at).getTime(),
|
||||
owner: repository.owner.login,
|
||||
repo: repository.name,
|
||||
pull_request_number: pullRequest.pullRequestNumber,
|
||||
comment_id: comment.id.toString(),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -48,8 +48,7 @@ type OnChecksFailedCallback = (
|
|||
type OnPullRequestReviewSubmitCallback = (
|
||||
repository: RepositoryWithGitHubRepository,
|
||||
pullRequest: PullRequest,
|
||||
review: ValidNotificationPullRequestReview,
|
||||
numberOfComments: number
|
||||
review: ValidNotificationPullRequestReview
|
||||
) => void
|
||||
|
||||
/**
|
||||
|
@ -105,6 +104,12 @@ export class NotificationsStore {
|
|||
public onNotificationEventReceived: NotificationCallback<DesktopAliveEvent> =
|
||||
async (event, id, userInfo) => this.handleAliveEvent(userInfo, true)
|
||||
|
||||
public simulateAliveEvent(event: DesktopAliveEvent) {
|
||||
if (__DEV__) {
|
||||
this.handleAliveEvent(event, false)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAliveEvent(
|
||||
e: DesktopAliveEvent,
|
||||
skipNotification: boolean
|
||||
|
@ -175,12 +180,7 @@ export class NotificationsStore {
|
|||
const onClick = () => {
|
||||
this.statsStore.recordPullRequestReviewNotificationClicked(review.state)
|
||||
|
||||
this.onPullRequestReviewSubmitCallback?.(
|
||||
repository,
|
||||
pullRequest,
|
||||
review,
|
||||
event.number_of_comments
|
||||
)
|
||||
this.onPullRequestReviewSubmitCallback?.(repository, pullRequest, review)
|
||||
}
|
||||
|
||||
if (skipNotification) {
|
||||
|
|
|
@ -729,7 +729,13 @@ function createWindow() {
|
|||
electron: '>=1.2.1',
|
||||
}
|
||||
|
||||
const extensions = [REACT_DEVELOPER_TOOLS, ChromeLens]
|
||||
const axeDevTools = {
|
||||
id: 'lhdoppojpmngadmnindnejefpokejbdd',
|
||||
electron: '>=1.2.1',
|
||||
Permissions: ['tabs', 'debugger'],
|
||||
}
|
||||
|
||||
const extensions = [REACT_DEVELOPER_TOOLS, ChromeLens, axeDevTools]
|
||||
|
||||
for (const extension of extensions) {
|
||||
try {
|
||||
|
|
|
@ -89,6 +89,7 @@ export enum PopupType {
|
|||
StartPullRequest = 'StartPullRequest',
|
||||
Error = 'Error',
|
||||
InstallingUpdate = 'InstallingUpdate',
|
||||
TestNotifications = 'TestNotifications',
|
||||
}
|
||||
|
||||
interface IBasePopup {
|
||||
|
@ -361,7 +362,6 @@ export type PopupDetail =
|
|||
repository: RepositoryWithGitHubRepository
|
||||
pullRequest: PullRequest
|
||||
review: ValidNotificationPullRequestReview
|
||||
numberOfComments: number
|
||||
shouldCheckoutBranch: boolean
|
||||
shouldChangeRepository: boolean
|
||||
}
|
||||
|
@ -389,5 +389,9 @@ export type PopupDetail =
|
|||
| {
|
||||
type: PopupType.InstallingUpdate
|
||||
}
|
||||
| {
|
||||
type: PopupType.TestNotifications
|
||||
repository: RepositoryWithGitHubRepository
|
||||
}
|
||||
|
||||
export type Popup = IBasePopup & PopupDetail
|
||||
|
|
|
@ -148,7 +148,6 @@ import { WarnForcePushDialog } from './multi-commit-operation/dialog/warn-force-
|
|||
import { clamp } from '../lib/clamp'
|
||||
import { generateRepositoryListContextMenu } from './repositories-list/repository-list-item-context-menu'
|
||||
import * as ipcRenderer from '../lib/ipc-renderer'
|
||||
import { showNotification } from '../lib/notifications/show-notification'
|
||||
import { DiscardChangesRetryDialog } from './discard-changes/discard-changes-retry-dialog'
|
||||
import { generateDevReleaseSummary } from '../lib/release-notes'
|
||||
import { PullRequestReview } from './notifications/pull-request-review'
|
||||
|
@ -164,6 +163,8 @@ import { uuid } from '../lib/uuid'
|
|||
import { InstallingUpdate } from './installing-update/installing-update'
|
||||
import { enableStackedPopups } from '../lib/feature-flag'
|
||||
import { DialogStackContext } from './dialog'
|
||||
import { TestNotifications } from './test-notifications/test-notifications'
|
||||
import { NotificationsDebugStore } from '../lib/stores/notifications-debug-store'
|
||||
|
||||
const MinuteInMilliseconds = 1000 * 60
|
||||
const HourInMilliseconds = MinuteInMilliseconds * 60
|
||||
|
@ -185,6 +186,7 @@ interface IAppProps {
|
|||
readonly issuesStore: IssuesStore
|
||||
readonly gitHubUserStore: GitHubUserStore
|
||||
readonly aheadBehindStore: AheadBehindStore
|
||||
readonly notificationsDebugStore: NotificationsDebugStore
|
||||
readonly startTime: number
|
||||
}
|
||||
|
||||
|
@ -489,10 +491,19 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return
|
||||
}
|
||||
|
||||
showNotification({
|
||||
title: 'Test notification',
|
||||
body: 'Click here! This is a test notification',
|
||||
onClick: () => this.props.dispatcher.showPopup({ type: PopupType.About }),
|
||||
// if current repository is not repository with github repository, return
|
||||
const repository = this.getRepository()
|
||||
if (
|
||||
repository == null ||
|
||||
repository instanceof CloningRepository ||
|
||||
!isRepositoryWithGitHubRepository(repository)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.dispatcher.showPopup({
|
||||
type: PopupType.TestNotifications,
|
||||
repository,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2262,14 +2273,13 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
case PopupType.PullRequestReview: {
|
||||
return (
|
||||
<PullRequestReview
|
||||
key="pull-request-checks-failed"
|
||||
key="pull-request-review"
|
||||
dispatcher={this.props.dispatcher}
|
||||
shouldCheckoutBranch={popup.shouldCheckoutBranch}
|
||||
shouldChangeRepository={popup.shouldChangeRepository}
|
||||
repository={popup.repository}
|
||||
pullRequest={popup.pullRequest}
|
||||
review={popup.review}
|
||||
numberOfComments={popup.numberOfComments}
|
||||
emoji={this.state.emoji}
|
||||
accounts={this.state.accounts}
|
||||
onSubmit={onPopupDismissedFn}
|
||||
|
@ -2374,6 +2384,17 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
/>
|
||||
)
|
||||
}
|
||||
case PopupType.TestNotifications: {
|
||||
return (
|
||||
<TestNotifications
|
||||
key="test-notifications"
|
||||
dispatcher={this.props.dispatcher}
|
||||
notificationsDebugStore={this.props.notificationsDebugStore}
|
||||
repository={popup.repository}
|
||||
onDismissed={onPopupDismissedFn}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default:
|
||||
return assertNever(popup, `Unknown popup type: ${popup}`)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React from 'react'
|
||||
import { Select } from '../lib/select'
|
||||
import { Button } from '../lib/button'
|
||||
|
@ -73,10 +71,17 @@ export class CommitMessageAvatar extends React.Component<
|
|||
public render() {
|
||||
return (
|
||||
<div className="commit-message-avatar-component">
|
||||
<div onClick={this.onAvatarClick}>
|
||||
{this.props.warningBadgeVisible && this.renderWarningBadge()}
|
||||
{this.props.warningBadgeVisible && (
|
||||
<Button className="avatar-button" onClick={this.onAvatarClick}>
|
||||
{this.renderWarningBadge()}
|
||||
<Avatar user={this.props.user} title={this.props.title} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!this.props.warningBadgeVisible && (
|
||||
<Avatar user={this.props.user} title={this.props.title} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.isPopoverOpen && this.renderPopover()}
|
||||
</div>
|
||||
)
|
||||
|
@ -108,7 +113,7 @@ export class CommitMessageAvatar extends React.Component<
|
|||
})
|
||||
}
|
||||
|
||||
private onAvatarClick = (event: React.FormEvent<HTMLDivElement>) => {
|
||||
private onAvatarClick = (event: React.FormEvent<HTMLButtonElement>) => {
|
||||
if (this.props.warningBadgeVisible === false) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -383,7 +383,7 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
|||
* 4. Any remaining button
|
||||
*
|
||||
*/
|
||||
private focusFirstSuitableChild() {
|
||||
public focusFirstSuitableChild() {
|
||||
const dialog = this.dialogElement
|
||||
|
||||
if (dialog === null) {
|
||||
|
|
|
@ -536,6 +536,7 @@ export class CommitSummary extends React.Component<
|
|||
let filesAdded = 0
|
||||
let filesModified = 0
|
||||
let filesRemoved = 0
|
||||
let filesRenamed = 0
|
||||
for (const file of this.props.changesetData.files) {
|
||||
switch (file.status.kind) {
|
||||
case AppFileStatusKind.New:
|
||||
|
@ -547,9 +548,14 @@ export class CommitSummary extends React.Component<
|
|||
case AppFileStatusKind.Deleted:
|
||||
filesRemoved += 1
|
||||
break
|
||||
case AppFileStatusKind.Renamed:
|
||||
filesRenamed += 1
|
||||
}
|
||||
}
|
||||
|
||||
const hasFileDescription =
|
||||
filesAdded + filesModified + filesRemoved + filesRenamed > 0
|
||||
|
||||
const filesLongDescription = (
|
||||
<>
|
||||
{filesAdded > 0 ? (
|
||||
|
@ -579,6 +585,15 @@ export class CommitSummary extends React.Component<
|
|||
{filesRemoved} deleted
|
||||
</span>
|
||||
) : null}
|
||||
{filesRenamed > 0 ? (
|
||||
<span>
|
||||
<Octicon
|
||||
className="files-renamed-icon"
|
||||
symbol={OcticonSymbol.diffRenamed}
|
||||
/>
|
||||
{filesRenamed} renamed
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
|
||||
|
@ -586,7 +601,9 @@ export class CommitSummary extends React.Component<
|
|||
<TooltippedContent
|
||||
className="commit-summary-meta-item without-truncation"
|
||||
tooltipClassName="changed-files-description-tooltip"
|
||||
tooltip={fileCount > 0 ? filesLongDescription : undefined}
|
||||
tooltip={
|
||||
fileCount > 0 && hasFileDescription ? filesLongDescription : undefined
|
||||
}
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.diff} />
|
||||
{filesShortDescription}
|
||||
|
|
|
@ -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 { NotificationsDebugStore } from '../lib/stores/notifications-debug-store'
|
||||
|
||||
if (__DEV__) {
|
||||
installDevGlobals()
|
||||
|
@ -267,6 +268,12 @@ const notificationsStore = new NotificationsStore(
|
|||
statsStore
|
||||
)
|
||||
|
||||
const notificationsDebugStore = new NotificationsDebugStore(
|
||||
accountsStore,
|
||||
notificationsStore,
|
||||
pullRequestCoordinator
|
||||
)
|
||||
|
||||
const appStore = new AppStore(
|
||||
gitHubUserStore,
|
||||
cloningRepositoriesStore,
|
||||
|
@ -353,6 +360,7 @@ ReactDOM.render(
|
|||
issuesStore={issuesStore}
|
||||
gitHubUserStore={gitHubUserStore}
|
||||
aheadBehindStore={aheadBehindStore}
|
||||
notificationsDebugStore={notificationsDebugStore}
|
||||
startTime={startTime}
|
||||
/>,
|
||||
document.getElementById('desktop-app-container')!
|
||||
|
|
200
app/src/ui/notifications/pull-request-comment-like.tsx
Normal file
200
app/src/ui/notifications/pull-request-comment-like.tsx
Normal file
|
@ -0,0 +1,200 @@
|
|||
import * as React from 'react'
|
||||
import { Dialog, DialogContent, DialogFooter } from '../dialog'
|
||||
import { PullRequest } from '../../models/pull-request'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import { Account } from '../../models/account'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { OcticonSymbolType } from '../octicons/octicons.generated'
|
||||
import { RepositoryWithGitHubRepository } from '../../models/repository'
|
||||
import { SandboxedMarkdown } from '../lib/sandboxed-markdown'
|
||||
import { LinkButton } from '../lib/link-button'
|
||||
import classNames from 'classnames'
|
||||
import { Avatar } from '../lib/avatar'
|
||||
import { formatRelative } from '../../lib/format-relative'
|
||||
import { getStealthEmailForUser } from '../../lib/email'
|
||||
import { IAPIIdentity } from '../../lib/api'
|
||||
|
||||
interface IPullRequestCommentLikeProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
readonly accounts: ReadonlyArray<Account>
|
||||
readonly repository: RepositoryWithGitHubRepository
|
||||
readonly pullRequest: PullRequest
|
||||
readonly eventDate: Date
|
||||
readonly eventVerb: string
|
||||
readonly eventIconSymbol: OcticonSymbolType
|
||||
readonly eventIconClass: string
|
||||
readonly externalURL: string
|
||||
readonly user: IAPIIdentity
|
||||
readonly body: string
|
||||
|
||||
/** Map from the emoji shortcut (e.g., :+1:) to the image's local path. */
|
||||
readonly emoji: Map<string, string>
|
||||
|
||||
readonly switchingToPullRequest: boolean
|
||||
|
||||
readonly renderFooterContent: () => JSX.Element
|
||||
|
||||
readonly onSubmit: () => void
|
||||
readonly onDismissed: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog to show a pull request review.
|
||||
*/
|
||||
export abstract class PullRequestCommentLike extends React.Component<IPullRequestCommentLikeProps> {
|
||||
public render() {
|
||||
const { title, pullRequestNumber } = this.props.pullRequest
|
||||
|
||||
const header = (
|
||||
<div className="pull-request-comment-like-dialog-header">
|
||||
{this.renderPullRequestIcon()}
|
||||
<span className="pr-title">
|
||||
<span className="pr-title">{title}</span>{' '}
|
||||
<span className="pr-number">#{pullRequestNumber}</span>{' '}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id="pull-request-review"
|
||||
type="normal"
|
||||
title={header}
|
||||
dismissable={false}
|
||||
onSubmit={this.props.onSubmit}
|
||||
onDismissed={this.props.onDismissed}
|
||||
loading={this.props.switchingToPullRequest}
|
||||
>
|
||||
<DialogContent>
|
||||
<div className="comment-container">
|
||||
{this.renderTimelineItem()}
|
||||
{this.renderCommentBubble()}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>{this.props.renderFooterContent()}</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
private renderTimelineItem() {
|
||||
const { user, repository, eventDate, eventVerb, externalURL } = this.props
|
||||
const { endpoint } = repository.gitHubRepository
|
||||
const userAvatar = {
|
||||
name: user.login,
|
||||
email: getStealthEmailForUser(user.id, user.login, endpoint),
|
||||
avatarURL: user.avatar_url,
|
||||
endpoint: endpoint,
|
||||
}
|
||||
|
||||
const bottomLine = this.shouldRenderCommentBubble()
|
||||
? null
|
||||
: this.renderDashedTimelineLine('bottom')
|
||||
|
||||
const timelineItemClass = classNames('timeline-item', {
|
||||
'with-comment': this.shouldRenderCommentBubble(),
|
||||
})
|
||||
|
||||
const diff = eventDate.getTime() - Date.now()
|
||||
const relativeReviewDate = formatRelative(diff)
|
||||
|
||||
return (
|
||||
<div className="timeline-item-container">
|
||||
{this.renderDashedTimelineLine('top')}
|
||||
<div className={timelineItemClass}>
|
||||
<Avatar user={userAvatar} title={null} size={40} />
|
||||
{this.renderReviewIcon()}
|
||||
<div className="summary">
|
||||
<LinkButton uri={user.html_url} className="author">
|
||||
{user.login}
|
||||
</LinkButton>{' '}
|
||||
{eventVerb} your pull request{' '}
|
||||
<LinkButton uri={externalURL} className="submission-date">
|
||||
{relativeReviewDate}
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
{bottomLine}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private shouldRenderCommentBubble() {
|
||||
return this.props.body !== ''
|
||||
}
|
||||
|
||||
private renderCommentBubble() {
|
||||
if (!this.shouldRenderCommentBubble()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="comment-bubble-container">
|
||||
<div className="comment-bubble">{this.renderReviewBody()}</div>
|
||||
{this.renderDashedTimelineLine('bottom')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderDashedTimelineLine(type: 'top' | 'bottom') {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`timeline-line ${type}`}
|
||||
>
|
||||
{/* Need to use 0.5 for X to prevent nearest neighbour filtering causing
|
||||
the line to appear semi-transparent. */}
|
||||
<line x1="0.5" y1="0" x2="0.5" y2="100%" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
private onMarkdownLinkClicked = (url: string) => {
|
||||
this.props.dispatcher.openInBrowser(url)
|
||||
}
|
||||
|
||||
private renderReviewBody() {
|
||||
const { body, emoji, pullRequest } = this.props
|
||||
const { base } = pullRequest
|
||||
|
||||
return (
|
||||
<SandboxedMarkdown
|
||||
markdown={body}
|
||||
emoji={emoji}
|
||||
baseHref={base.gitHubRepository.htmlURL ?? undefined}
|
||||
repository={base.gitHubRepository}
|
||||
onMarkdownLinkClicked={this.onMarkdownLinkClicked}
|
||||
markdownContext={'PullRequestComment'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderPullRequestIcon = () => {
|
||||
const { pullRequest } = this.props
|
||||
|
||||
const cls = classNames('pull-request-icon', {
|
||||
draft: pullRequest.draft,
|
||||
})
|
||||
|
||||
return (
|
||||
<Octicon
|
||||
className={cls}
|
||||
symbol={
|
||||
pullRequest.draft
|
||||
? OcticonSymbol.gitPullRequestDraft
|
||||
: OcticonSymbol.gitPullRequest
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderReviewIcon = () => {
|
||||
const { eventIconSymbol, eventIconClass } = this.props
|
||||
|
||||
return (
|
||||
<div className={classNames('review-icon-container', eventIconClass)}>
|
||||
<Octicon symbol={eventIconSymbol} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,24 +1,17 @@
|
|||
import * as React from 'react'
|
||||
import { Dialog, DialogContent, DialogFooter } from '../dialog'
|
||||
import { Row } from '../lib/row'
|
||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { PullRequest } from '../../models/pull-request'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import { Account } from '../../models/account'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { RepositoryWithGitHubRepository } from '../../models/repository'
|
||||
import { SandboxedMarkdown } from '../lib/sandboxed-markdown'
|
||||
import {
|
||||
getPullRequestReviewStateIcon,
|
||||
getVerbForPullRequestReview,
|
||||
} from './pull-request-review-helpers'
|
||||
import { LinkButton } from '../lib/link-button'
|
||||
import classNames from 'classnames'
|
||||
import { Avatar } from '../lib/avatar'
|
||||
import { formatRelative } from '../../lib/format-relative'
|
||||
import { ValidNotificationPullRequestReview } from '../../lib/valid-notification-pull-request-review'
|
||||
import { getStealthEmailForUser } from '../../lib/email'
|
||||
import { PullRequestCommentLike } from './pull-request-comment-like'
|
||||
|
||||
interface IPullRequestReviewProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
|
@ -26,7 +19,6 @@ interface IPullRequestReviewProps {
|
|||
readonly repository: RepositoryWithGitHubRepository
|
||||
readonly pullRequest: PullRequest
|
||||
readonly review: ValidNotificationPullRequestReview
|
||||
readonly numberOfComments: number
|
||||
|
||||
/** Map from the emoji shortcut (e.g., :+1:) to the image's local path. */
|
||||
readonly emoji: Map<string, string>
|
||||
|
@ -47,7 +39,7 @@ interface IPullRequestReviewState {
|
|||
}
|
||||
|
||||
/**
|
||||
* Dialog to show the result of a CI check run.
|
||||
* Dialog to show a pull request review.
|
||||
*/
|
||||
export class PullRequestReview extends React.Component<
|
||||
IPullRequestReviewProps,
|
||||
|
@ -62,115 +54,42 @@ export class PullRequestReview extends React.Component<
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { title, pullRequestNumber } = this.props.pullRequest
|
||||
const {
|
||||
dispatcher,
|
||||
accounts,
|
||||
repository,
|
||||
pullRequest,
|
||||
emoji,
|
||||
review,
|
||||
onSubmit,
|
||||
onDismissed,
|
||||
} = this.props
|
||||
|
||||
const header = (
|
||||
<div className="pull-request-review-dialog-header">
|
||||
{this.renderPullRequestIcon()}
|
||||
<span className="pr-title">
|
||||
<span className="pr-title">{title}</span>{' '}
|
||||
<span className="pr-number">#{pullRequestNumber}</span>{' '}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
const icon = getPullRequestReviewStateIcon(review.state)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id="pull-request-review"
|
||||
type="normal"
|
||||
title={header}
|
||||
dismissable={false}
|
||||
onSubmit={this.props.onSubmit}
|
||||
onDismissed={this.props.onDismissed}
|
||||
loading={this.state.switchingToPullRequest}
|
||||
>
|
||||
<DialogContent>
|
||||
<div className="review-container">
|
||||
{this.renderTimelineItem()}
|
||||
{this.renderCommentBubble()}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>{this.renderFooterContent()}</DialogFooter>
|
||||
</Dialog>
|
||||
<PullRequestCommentLike
|
||||
dispatcher={dispatcher}
|
||||
accounts={accounts}
|
||||
repository={repository}
|
||||
pullRequest={pullRequest}
|
||||
emoji={emoji}
|
||||
eventDate={new Date(review.submitted_at)}
|
||||
eventVerb={getVerbForPullRequestReview(review)}
|
||||
eventIconSymbol={icon.symbol}
|
||||
eventIconClass={icon.className}
|
||||
externalURL={review.html_url}
|
||||
user={review.user}
|
||||
body={review.body}
|
||||
switchingToPullRequest={this.state.switchingToPullRequest}
|
||||
renderFooterContent={this.renderFooterContent}
|
||||
onSubmit={onSubmit}
|
||||
onDismissed={onDismissed}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderTimelineItem() {
|
||||
const { review, repository } = this.props
|
||||
const { user } = review
|
||||
const { endpoint } = repository.gitHubRepository
|
||||
const verb = getVerbForPullRequestReview(review)
|
||||
const userAvatar = {
|
||||
name: user.login,
|
||||
email: getStealthEmailForUser(user.id, user.login, endpoint),
|
||||
avatarURL: user.avatar_url,
|
||||
endpoint: endpoint,
|
||||
}
|
||||
|
||||
const bottomLine = this.shouldRenderCommentBubble()
|
||||
? null
|
||||
: this.renderDashedTimelineLine('bottom')
|
||||
|
||||
const timelineItemClass = classNames('timeline-item', {
|
||||
'with-comment': this.shouldRenderCommentBubble(),
|
||||
})
|
||||
|
||||
const submittedAt = new Date(review.submitted_at)
|
||||
const diff = submittedAt.getTime() - Date.now()
|
||||
const relativeReviewDate = formatRelative(diff)
|
||||
|
||||
return (
|
||||
<div className="timeline-item-container">
|
||||
{this.renderDashedTimelineLine('top')}
|
||||
<div className={timelineItemClass}>
|
||||
<Avatar user={userAvatar} title={null} size={40} />
|
||||
{this.renderReviewIcon()}
|
||||
<div className="summary">
|
||||
<LinkButton uri={review.user.html_url} className="reviewer">
|
||||
{review.user.login}
|
||||
</LinkButton>{' '}
|
||||
{verb} your pull request{' '}
|
||||
<LinkButton uri={review.html_url} className="submission-date">
|
||||
{relativeReviewDate}
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
{bottomLine}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private shouldRenderCommentBubble() {
|
||||
return this.props.review.body !== ''
|
||||
}
|
||||
|
||||
private renderCommentBubble() {
|
||||
if (!this.shouldRenderCommentBubble()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="comment-bubble-container">
|
||||
<div className="comment-bubble">{this.renderReviewBody()}</div>
|
||||
{this.renderDashedTimelineLine('bottom')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderDashedTimelineLine(type: 'top' | 'bottom') {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`timeline-line ${type}`}
|
||||
>
|
||||
{/* Need to use 0.5 for X to prevent nearest neighbour filtering causing
|
||||
the line to appear semi-transparent. */}
|
||||
<line x1="0.5" y1="0" x2="0.5" y2="100%" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
private renderFooterContent() {
|
||||
private renderFooterContent = () => {
|
||||
const { review, shouldChangeRepository, shouldCheckoutBranch } = this.props
|
||||
const isApprovedReview = review.state === 'APPROVED'
|
||||
|
||||
|
@ -213,56 +132,6 @@ export class PullRequestReview extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private onMarkdownLinkClicked = (url: string) => {
|
||||
this.props.dispatcher.openInBrowser(url)
|
||||
}
|
||||
|
||||
private renderReviewBody() {
|
||||
const { review, emoji, pullRequest } = this.props
|
||||
const { base } = pullRequest
|
||||
|
||||
return (
|
||||
<SandboxedMarkdown
|
||||
markdown={review.body}
|
||||
emoji={emoji}
|
||||
baseHref={base.gitHubRepository.htmlURL ?? undefined}
|
||||
repository={base.gitHubRepository}
|
||||
onMarkdownLinkClicked={this.onMarkdownLinkClicked}
|
||||
markdownContext={'PullRequestComment'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderPullRequestIcon = () => {
|
||||
const { pullRequest } = this.props
|
||||
|
||||
const cls = classNames('pull-request-icon', {
|
||||
draft: pullRequest.draft,
|
||||
})
|
||||
|
||||
return (
|
||||
<Octicon
|
||||
className={cls}
|
||||
symbol={
|
||||
pullRequest.draft
|
||||
? OcticonSymbol.gitPullRequestDraft
|
||||
: OcticonSymbol.gitPullRequest
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderReviewIcon = () => {
|
||||
const { review } = this.props
|
||||
|
||||
const icon = getPullRequestReviewStateIcon(review.state)
|
||||
return (
|
||||
<div className={classNames('review-icon-container', icon.className)}>
|
||||
<Octicon symbol={icon.symbol} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onSubmit = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
|
||||
|
|
|
@ -41,6 +41,8 @@ const SignInWithBrowserTitle = __DARWIN__
|
|||
const DefaultTitle = 'Sign in'
|
||||
|
||||
export class SignIn extends React.Component<ISignInProps, ISignInState> {
|
||||
private readonly dialogRef = React.createRef<Dialog>()
|
||||
|
||||
public constructor(props: ISignInProps) {
|
||||
super(props)
|
||||
|
||||
|
@ -52,6 +54,18 @@ export class SignIn extends React.Component<ISignInProps, ISignInState> {
|
|||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: ISignInProps) {
|
||||
// Whenever the sign in step changes we replace the dialog contents which
|
||||
// means we need to re-focus the first suitable child element as it's
|
||||
// essentially a "new" dialog we're showing only the dialog component itself
|
||||
// doesn't know that.
|
||||
if (prevProps.signInState !== null && this.props.signInState !== null) {
|
||||
if (prevProps.signInState.kind !== this.props.signInState.kind) {
|
||||
this.dialogRef.current?.focusFirstSuitableChild()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: ISignInProps) {
|
||||
if (nextProps.signInState !== this.props.signInState) {
|
||||
if (
|
||||
|
@ -161,6 +175,7 @@ export class SignIn extends React.Component<ISignInProps, ISignInState> {
|
|||
<OkCancelButtonGroup
|
||||
okButtonText={primaryButtonText}
|
||||
okButtonDisabled={disableSubmit}
|
||||
onCancelButtonClick={this.onDismissed}
|
||||
/>
|
||||
</DialogFooter>
|
||||
)
|
||||
|
@ -324,6 +339,7 @@ export class SignIn extends React.Component<ISignInProps, ISignInState> {
|
|||
onDismissed={this.onDismissed}
|
||||
onSubmit={this.onSubmit}
|
||||
loading={state.loading}
|
||||
ref={this.dialogRef}
|
||||
>
|
||||
{errors}
|
||||
{this.renderStep()}
|
||||
|
|
666
app/src/ui/test-notifications/test-notifications.tsx
Normal file
666
app/src/ui/test-notifications/test-notifications.tsx
Normal file
|
@ -0,0 +1,666 @@
|
|||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import { getHTMLURL, IAPIComment } from '../../lib/api'
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
import { NotificationsDebugStore } from '../../lib/stores/notifications-debug-store'
|
||||
import {
|
||||
ValidNotificationPullRequestReview,
|
||||
ValidNotificationPullRequestReviewState,
|
||||
} from '../../lib/valid-notification-pull-request-review'
|
||||
import { PullRequest } from '../../models/pull-request'
|
||||
import { RepositoryWithGitHubRepository } from '../../models/repository'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
OkCancelButtonGroup,
|
||||
} from '../dialog'
|
||||
import { DialogHeader } from '../dialog/header'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import { Button } from '../lib/button'
|
||||
import { List } from '../lib/list'
|
||||
import { Loading } from '../lib/loading'
|
||||
import { getPullRequestReviewStateIcon } from '../notifications/pull-request-review-helpers'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
|
||||
enum TestNotificationType {
|
||||
PullRequestReview,
|
||||
PullRequestReviewComment,
|
||||
}
|
||||
|
||||
enum TestNotificationStepKind {
|
||||
SelectPullRequest,
|
||||
SelectPullRequestReview,
|
||||
SelectPullRequestComment,
|
||||
}
|
||||
|
||||
type TestNotificationFlow = {
|
||||
readonly type: TestNotificationType
|
||||
readonly steps: ReadonlyArray<TestNotificationStepKind>
|
||||
}
|
||||
|
||||
const TestNotificationFlows: ReadonlyArray<TestNotificationFlow> = [
|
||||
{
|
||||
type: TestNotificationType.PullRequestReview,
|
||||
steps: [
|
||||
TestNotificationStepKind.SelectPullRequest,
|
||||
TestNotificationStepKind.SelectPullRequestReview,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: TestNotificationType.PullRequestReviewComment,
|
||||
steps: [
|
||||
TestNotificationStepKind.SelectPullRequest,
|
||||
TestNotificationStepKind.SelectPullRequestComment,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
type TestNotificationStepSelectPullRequestResult = {
|
||||
readonly kind: TestNotificationStepKind.SelectPullRequest
|
||||
readonly pullRequest: PullRequest
|
||||
}
|
||||
type TestNotificationStepSelectPullRequestReviewResult = {
|
||||
readonly kind: TestNotificationStepKind.SelectPullRequestReview
|
||||
readonly review: ValidNotificationPullRequestReview
|
||||
}
|
||||
type TestNotificationStepSelectPullRequestCommentResult = {
|
||||
readonly kind: TestNotificationStepKind.SelectPullRequestComment
|
||||
readonly comment: IAPIComment
|
||||
readonly isIssueComment: boolean
|
||||
}
|
||||
|
||||
type TestNotificationStepResultMap = Map<
|
||||
TestNotificationStepKind.SelectPullRequest,
|
||||
TestNotificationStepSelectPullRequestResult
|
||||
> &
|
||||
Map<
|
||||
TestNotificationStepKind.SelectPullRequestReview,
|
||||
TestNotificationStepSelectPullRequestReviewResult
|
||||
> &
|
||||
Map<
|
||||
TestNotificationStepKind.SelectPullRequestComment,
|
||||
TestNotificationStepSelectPullRequestCommentResult
|
||||
>
|
||||
|
||||
interface ITestNotificationsState {
|
||||
readonly selectedFlow: TestNotificationFlow | null
|
||||
readonly stepResults: TestNotificationStepResultMap
|
||||
readonly loading: boolean
|
||||
readonly pullRequests: ReadonlyArray<PullRequest>
|
||||
readonly reviews: ReadonlyArray<ValidNotificationPullRequestReview>
|
||||
readonly comments: ReadonlyArray<IAPIComment>
|
||||
}
|
||||
|
||||
interface ITestNotificationsProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
readonly notificationsDebugStore: NotificationsDebugStore
|
||||
readonly repository: RepositoryWithGitHubRepository
|
||||
readonly onDismissed: () => void
|
||||
}
|
||||
|
||||
class TestNotificationItemRowContent extends React.Component<{
|
||||
readonly leftAccessory?: JSX.Element
|
||||
readonly html_url?: string
|
||||
readonly dispatcher: Dispatcher
|
||||
}> {
|
||||
public render() {
|
||||
const { leftAccessory, html_url, children } = this.props
|
||||
|
||||
return (
|
||||
<div className="row-content">
|
||||
{leftAccessory && <div className="left-accessory">{leftAccessory}</div>}
|
||||
<div className="main-content">{children}</div>
|
||||
{html_url && (
|
||||
<div className="right-accessory">
|
||||
<Button onClick={this.onExternalLinkClick}>
|
||||
<Octicon symbol={OcticonSymbol.linkExternal} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onExternalLinkClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const { dispatcher, html_url } = this.props
|
||||
|
||||
if (html_url === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
e.stopPropagation()
|
||||
dispatcher.openInBrowser(html_url)
|
||||
}
|
||||
}
|
||||
|
||||
export class TestNotifications extends React.Component<
|
||||
ITestNotificationsProps,
|
||||
ITestNotificationsState
|
||||
> {
|
||||
public constructor(props: ITestNotificationsProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
selectedFlow: null,
|
||||
stepResults: new Map(),
|
||||
loading: false,
|
||||
pullRequests: [],
|
||||
reviews: [],
|
||||
comments: [],
|
||||
}
|
||||
}
|
||||
|
||||
private onDismissed = () => {
|
||||
this.props.dispatcher.closePopup()
|
||||
}
|
||||
|
||||
private renderNotificationType = (
|
||||
type: TestNotificationType
|
||||
): JSX.Element => {
|
||||
const title =
|
||||
type === TestNotificationType.PullRequestReview
|
||||
? 'Pull Request Review'
|
||||
: 'Pull Request Review Comment'
|
||||
|
||||
return (
|
||||
<Button onClick={this.getOnNotificationTypeClick(type)}>{title}</Button>
|
||||
)
|
||||
}
|
||||
|
||||
private getOnNotificationTypeClick = (type: TestNotificationType) => () => {
|
||||
const selectedFlow =
|
||||
TestNotificationFlows.find(f => f.type === type) ?? null
|
||||
|
||||
this.setState(
|
||||
{
|
||||
selectedFlow,
|
||||
},
|
||||
() => {
|
||||
this.prepareForNextStep()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private doFinalAction() {
|
||||
const selectedFlow = this.state.selectedFlow
|
||||
|
||||
if (selectedFlow === null) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (selectedFlow.type) {
|
||||
case TestNotificationType.PullRequestReview: {
|
||||
const pullRequestNumber = this.getPullRequest()
|
||||
const review = this.getReview()
|
||||
|
||||
if (pullRequestNumber === null || review === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.notificationsDebugStore.simulatePullRequestReviewNotification(
|
||||
this.props.repository.gitHubRepository,
|
||||
pullRequestNumber,
|
||||
review
|
||||
)
|
||||
break
|
||||
}
|
||||
case TestNotificationType.PullRequestReviewComment: {
|
||||
const pullRequest = this.getPullRequest()
|
||||
const commentInfo = this.getCommentInfo()
|
||||
|
||||
if (pullRequest === null || commentInfo === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { comment, isIssueComment } = commentInfo
|
||||
|
||||
this.props.notificationsDebugStore.simulatePullRequestCommentNotification(
|
||||
this.props.repository.gitHubRepository,
|
||||
pullRequest,
|
||||
comment,
|
||||
isIssueComment
|
||||
)
|
||||
break
|
||||
}
|
||||
default:
|
||||
assertNever(selectedFlow.type, `Unknown flow type: ${selectedFlow}`)
|
||||
}
|
||||
}
|
||||
|
||||
private prepareForNextStep() {
|
||||
const nextStep = this.state.selectedFlow?.steps[this.state.stepResults.size]
|
||||
|
||||
if (nextStep === undefined) {
|
||||
this.doFinalAction()
|
||||
this.back()
|
||||
return
|
||||
}
|
||||
|
||||
switch (nextStep) {
|
||||
case TestNotificationStepKind.SelectPullRequest: {
|
||||
this.setState({
|
||||
loading: true,
|
||||
})
|
||||
|
||||
this.props.notificationsDebugStore
|
||||
.getPullRequests(this.props.repository)
|
||||
.then(pullRequests => {
|
||||
this.setState({
|
||||
pullRequests,
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
case TestNotificationStepKind.SelectPullRequestReview: {
|
||||
this.setState({
|
||||
loading: true,
|
||||
})
|
||||
|
||||
const pullRequest = this.getPullRequest()
|
||||
|
||||
if (pullRequest === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.notificationsDebugStore
|
||||
.getPullRequestReviews(
|
||||
this.props.repository,
|
||||
pullRequest.pullRequestNumber
|
||||
)
|
||||
.then(reviews => {
|
||||
this.setState({
|
||||
reviews,
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
case TestNotificationStepKind.SelectPullRequestComment: {
|
||||
this.setState({
|
||||
loading: true,
|
||||
})
|
||||
|
||||
const pullRequest = this.getPullRequest()
|
||||
|
||||
if (pullRequest === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.notificationsDebugStore
|
||||
.getPullRequestComments(
|
||||
this.props.repository,
|
||||
pullRequest.pullRequestNumber
|
||||
)
|
||||
.then(comments => {
|
||||
this.setState({
|
||||
comments,
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
default:
|
||||
assertNever(nextStep, `Unknown step: ${nextStep}`)
|
||||
}
|
||||
}
|
||||
|
||||
private getPullRequest(): PullRequest | null {
|
||||
const pullRequestResult = this.state.stepResults.get(
|
||||
TestNotificationStepKind.SelectPullRequest
|
||||
)
|
||||
|
||||
if (pullRequestResult === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return pullRequestResult.pullRequest
|
||||
}
|
||||
|
||||
private getReview(): ValidNotificationPullRequestReview | null {
|
||||
const reviewResult = this.state.stepResults.get(
|
||||
TestNotificationStepKind.SelectPullRequestReview
|
||||
)
|
||||
|
||||
if (reviewResult === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return reviewResult.review
|
||||
}
|
||||
|
||||
private getCommentInfo() {
|
||||
const commentResult = this.state.stepResults.get(
|
||||
TestNotificationStepKind.SelectPullRequestComment
|
||||
)
|
||||
|
||||
if (commentResult === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
comment: commentResult.comment,
|
||||
isIssueComment: commentResult.isIssueComment,
|
||||
}
|
||||
}
|
||||
|
||||
private renderCurrentStep() {
|
||||
if (this.state.selectedFlow === null) {
|
||||
return (
|
||||
<div>
|
||||
<p>Select the type of notification to display:</p>
|
||||
<div className="notification-type-list">
|
||||
{this.renderNotificationType(
|
||||
TestNotificationType.PullRequestReview
|
||||
)}
|
||||
{this.renderNotificationType(
|
||||
TestNotificationType.PullRequestReviewComment
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const currentStep = this.state.selectedFlow.steps.at(
|
||||
this.state.stepResults.size
|
||||
)
|
||||
|
||||
if (currentStep === undefined) {
|
||||
return <p>Done!</p>
|
||||
}
|
||||
|
||||
switch (currentStep) {
|
||||
case TestNotificationStepKind.SelectPullRequest:
|
||||
return this.renderSelectPullRequest()
|
||||
case TestNotificationStepKind.SelectPullRequestReview:
|
||||
return this.renderSelectPullRequestReview()
|
||||
case TestNotificationStepKind.SelectPullRequestComment:
|
||||
return this.renderSelectPullRequestComment()
|
||||
default:
|
||||
return assertNever(currentStep, `Unknown step: ${currentStep}`)
|
||||
}
|
||||
}
|
||||
|
||||
private renderSelectPullRequest() {
|
||||
if (this.state.loading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
const { pullRequests } = this.state
|
||||
|
||||
if (pullRequests.length === 0) {
|
||||
return <p>No pull requests found</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
Pull requests:
|
||||
<List
|
||||
rowHeight={40}
|
||||
rowCount={pullRequests.length}
|
||||
rowRenderer={this.renderPullRequestRow}
|
||||
selectedRows={[]}
|
||||
onRowClick={this.onPullRequestRowClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onPullRequestRowClick = (row: number) => {
|
||||
const pullRequest = this.state.pullRequests[row]
|
||||
const stepResults = this.state.stepResults
|
||||
stepResults.set(TestNotificationStepKind.SelectPullRequest, {
|
||||
kind: TestNotificationStepKind.SelectPullRequest,
|
||||
pullRequest: pullRequest,
|
||||
})
|
||||
|
||||
this.setState(
|
||||
{
|
||||
stepResults,
|
||||
},
|
||||
() => {
|
||||
this.prepareForNextStep()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private renderSelectPullRequestReview() {
|
||||
if (this.state.loading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
const { reviews } = this.state
|
||||
|
||||
if (reviews.length === 0) {
|
||||
return <p>No reviews found</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
Reviews:
|
||||
<List
|
||||
rowHeight={40}
|
||||
rowCount={reviews.length}
|
||||
rowRenderer={this.renderPullRequestReviewRow}
|
||||
selectedRows={[]}
|
||||
onRowClick={this.onPullRequestReviewRowClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onPullRequestReviewRowClick = (row: number) => {
|
||||
const review = this.state.reviews[row]
|
||||
const stepResults = this.state.stepResults
|
||||
stepResults.set(TestNotificationStepKind.SelectPullRequestReview, {
|
||||
kind: TestNotificationStepKind.SelectPullRequestReview,
|
||||
review: review,
|
||||
})
|
||||
|
||||
this.setState(
|
||||
{
|
||||
stepResults,
|
||||
},
|
||||
() => {
|
||||
this.prepareForNextStep()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private renderSelectPullRequestComment() {
|
||||
if (this.state.loading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
const { comments } = this.state
|
||||
|
||||
if (comments.length === 0) {
|
||||
return <p>No comments found</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
Comments:
|
||||
<List
|
||||
rowHeight={40}
|
||||
rowCount={comments.length}
|
||||
rowRenderer={this.renderPullRequestCommentRow}
|
||||
selectedRows={[]}
|
||||
onRowClick={this.onPullRequestCommentRowClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onPullRequestCommentRowClick = (row: number) => {
|
||||
const comment = this.state.comments[row]
|
||||
const stepResults = this.state.stepResults
|
||||
stepResults.set(TestNotificationStepKind.SelectPullRequestComment, {
|
||||
kind: TestNotificationStepKind.SelectPullRequestComment,
|
||||
comment: comment,
|
||||
isIssueComment: comment.html_url.includes('#issuecomment-'),
|
||||
})
|
||||
|
||||
this.setState(
|
||||
{
|
||||
stepResults,
|
||||
},
|
||||
() => {
|
||||
this.prepareForNextStep()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private renderPullRequestCommentRow = (row: number) => {
|
||||
const comment = this.state.comments[row]
|
||||
return (
|
||||
<TestNotificationItemRowContent
|
||||
dispatcher={this.props.dispatcher}
|
||||
html_url={comment.html_url}
|
||||
leftAccessory={this.renderReviewStateIcon('COMMENTED')}
|
||||
>
|
||||
{comment.body}
|
||||
<br />
|
||||
by <i>{comment.user.login}</i>
|
||||
</TestNotificationItemRowContent>
|
||||
)
|
||||
}
|
||||
|
||||
private renderPullRequestReviewRow = (row: number) => {
|
||||
const review = this.state.reviews[row]
|
||||
|
||||
return (
|
||||
<TestNotificationItemRowContent
|
||||
dispatcher={this.props.dispatcher}
|
||||
html_url={review.html_url}
|
||||
leftAccessory={this.renderReviewStateIcon(review.state)}
|
||||
>
|
||||
{review.body || <i>Review without body</i>}
|
||||
<br />
|
||||
by <i>{review.user.login}</i>
|
||||
</TestNotificationItemRowContent>
|
||||
)
|
||||
}
|
||||
|
||||
private renderReviewStateIcon = (
|
||||
state: ValidNotificationPullRequestReviewState
|
||||
) => {
|
||||
const icon = getPullRequestReviewStateIcon(state)
|
||||
return (
|
||||
<div className={classNames('review-icon-container', icon.className)}>
|
||||
<Octicon symbol={icon.symbol} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderPullRequestRow = (row: number) => {
|
||||
const pullRequest = this.state.pullRequests[row]
|
||||
const repository = this.props.repository.gitHubRepository
|
||||
const endpointHtmlUrl = getHTMLURL(repository.endpoint)
|
||||
const htmlURL = `${endpointHtmlUrl}/${repository.owner.login}/${repository.name}/pull/${pullRequest.pullRequestNumber}`
|
||||
|
||||
return (
|
||||
<TestNotificationItemRowContent
|
||||
dispatcher={this.props.dispatcher}
|
||||
html_url={htmlURL}
|
||||
leftAccessory={this.renderPullRequestStateIcon(pullRequest)}
|
||||
>
|
||||
<b>
|
||||
#{pullRequest.pullRequestNumber}
|
||||
{pullRequest.draft ? ' (Draft)' : ''}:
|
||||
</b>{' '}
|
||||
{pullRequest.title} <br />
|
||||
by <i>{pullRequest.author}</i>
|
||||
</TestNotificationItemRowContent>
|
||||
)
|
||||
}
|
||||
|
||||
private renderPullRequestStateIcon = (
|
||||
pullRequest: PullRequest
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Octicon
|
||||
className={pullRequest.draft ? 'pr-draft-icon' : 'pr-icon'}
|
||||
symbol={
|
||||
pullRequest.draft
|
||||
? OcticonSymbol.gitPullRequestDraft
|
||||
: OcticonSymbol.gitPullRequest
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Dialog
|
||||
id="test-notifications"
|
||||
onSubmit={this.onDismissed}
|
||||
dismissable={true}
|
||||
onDismissed={this.onDismissed}
|
||||
>
|
||||
<DialogHeader
|
||||
title="Test Notifications"
|
||||
dismissable={true}
|
||||
onDismissed={this.onDismissed}
|
||||
/>
|
||||
<DialogContent>{this.renderCurrentStep()}</DialogContent>
|
||||
<DialogFooter>
|
||||
<OkCancelButtonGroup
|
||||
okButtonText="Close"
|
||||
okButtonDisabled={false}
|
||||
cancelButtonDisabled={false}
|
||||
cancelButtonVisible={this.state.selectedFlow !== null}
|
||||
cancelButtonText="Back"
|
||||
onCancelButtonClick={this.onBack}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
private onBack = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
this.back()
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
private back() {
|
||||
const { selectedFlow, stepResults } = this.state
|
||||
if (selectedFlow === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (stepResults.size === 0) {
|
||||
this.setState(
|
||||
{
|
||||
selectedFlow: null,
|
||||
stepResults: new Map(),
|
||||
},
|
||||
() => {
|
||||
this.prepareForNextStep()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const steps = selectedFlow.steps
|
||||
const lastStep = steps.at(stepResults.size - 1)
|
||||
if (lastStep === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const newStepResults: Map<TestNotificationStepKind, any> = new Map(
|
||||
stepResults
|
||||
)
|
||||
newStepResults.delete(lastStep)
|
||||
|
||||
this.setState(
|
||||
{
|
||||
stepResults: newStepResults as TestNotificationStepResultMap,
|
||||
},
|
||||
() => {
|
||||
this.prepareForNextStep()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -95,7 +95,6 @@
|
|||
@import 'ui/check-runs/_ci-check-run-popover';
|
||||
@import 'ui/check-runs/ci-check-run-job-steps';
|
||||
@import 'ui/_pull-request-checks-failed';
|
||||
@import 'ui/_pull-request-review';
|
||||
@import 'ui/_sandboxed-markdown';
|
||||
@import 'ui/_pull-request-quick-view';
|
||||
@import 'ui/discard-changes-retry';
|
||||
|
|
|
@ -2,6 +2,27 @@
|
|||
// With this, the popover's absolute position will be relative to its parent
|
||||
position: relative;
|
||||
|
||||
.avatar-button {
|
||||
// override default button styles
|
||||
overflow: inherit;
|
||||
text-overflow: inherit;
|
||||
white-space: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
padding: inherit;
|
||||
border: none;
|
||||
height: inherit;
|
||||
color: inherit;
|
||||
background-color: none;
|
||||
|
||||
border-radius: 50%;
|
||||
margin-right: var(--spacing-half);
|
||||
|
||||
.avatar {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-badge {
|
||||
background-color: var(--commit-warning-badge-background-color);
|
||||
border: var(--commit-warning-badge-border-color) 1px solid;
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
@import 'dialogs/unreachable-commits';
|
||||
@import 'dialogs/open-pull-request';
|
||||
@import 'dialogs/installing-update';
|
||||
@import 'dialogs/test-notifications';
|
||||
@import 'dialogs/pull-request-comment-like';
|
||||
|
||||
// The styles herein attempt to follow a flow where margins are only applied
|
||||
// to the bottom of elements (with the exception of the last child). This to
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#pull-request-review {
|
||||
#pull-request-review,
|
||||
#pull-request-comment {
|
||||
--avatar-size: 40px;
|
||||
min-width: 500px;
|
||||
|
||||
|
@ -6,7 +7,7 @@
|
|||
height: unset;
|
||||
}
|
||||
|
||||
.pull-request-review-dialog-header {
|
||||
.pull-request-comment-like-dialog-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -44,7 +45,7 @@
|
|||
max-height: 300px;
|
||||
overflow: auto;
|
||||
|
||||
.review-container {
|
||||
.comment-container {
|
||||
.timeline-line {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
|
@ -103,7 +104,7 @@
|
|||
.link-button-component {
|
||||
color: unset;
|
||||
|
||||
&.reviewer {
|
||||
&.author {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
75
app/styles/ui/dialogs/_test-notifications.scss
Normal file
75
app/styles/ui/dialogs/_test-notifications.scss
Normal file
|
@ -0,0 +1,75 @@
|
|||
#test-notifications {
|
||||
.notification-type-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: var(--spacing-half);
|
||||
}
|
||||
|
||||
.list {
|
||||
height: 200px;
|
||||
|
||||
.row-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.left-accessory {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.right-accessory {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.review-icon-container {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.octicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.pr-review-approved {
|
||||
color: var(--pr-approved-icon-color);
|
||||
background-color: var(--pr-approved-icon-background-color);
|
||||
}
|
||||
&.pr-review-changes-requested {
|
||||
color: var(--pr-changes-requested-icon-color);
|
||||
background-color: var(--pr-changes-requested-icon-background-color);
|
||||
}
|
||||
&.pr-review-commented {
|
||||
color: var(--pr-commented-icon-color);
|
||||
background-color: var(--pr-commented-icon-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.pr-icon {
|
||||
color: var(--pr-open-icon-color);
|
||||
}
|
||||
|
||||
.pr-draft-icon {
|
||||
color: var(--pr-draft-icon-color);
|
||||
}
|
||||
}
|
|
@ -193,6 +193,10 @@ body > .tooltip,
|
|||
color: var(--color-deleted);
|
||||
}
|
||||
|
||||
.files-renamed-icon {
|
||||
color: var(--color-renamed);
|
||||
}
|
||||
|
||||
.octicon {
|
||||
margin-right: var(--spacing-third);
|
||||
vertical-align: bottom; // For some reason, `bottom` places the text in the middle
|
||||
|
|
|
@ -32,6 +32,7 @@ import { CommitStatusStore } from '../../src/lib/stores/commit-status-store'
|
|||
import { AheadBehindStore } from '../../src/lib/stores/ahead-behind-store'
|
||||
import { AliveStore } from '../../src/lib/stores/alive-store'
|
||||
import { NotificationsStore } from '../../src/lib/stores/notifications-store'
|
||||
import { NotificationsDebugStore } from '../../src/lib/stores/notifications-debug-store'
|
||||
|
||||
describe('App', () => {
|
||||
let appStore: AppStore
|
||||
|
@ -41,6 +42,7 @@ describe('App', () => {
|
|||
let githubUserStore: GitHubUserStore
|
||||
let issuesStore: IssuesStore
|
||||
let aheadBehindStore: AheadBehindStore
|
||||
let notificationsDebugStore: NotificationsDebugStore
|
||||
|
||||
beforeEach(async () => {
|
||||
const db = new TestGitHubUserDatabase()
|
||||
|
@ -86,6 +88,12 @@ describe('App', () => {
|
|||
)
|
||||
notificationsStore.setNotificationsEnabled(false)
|
||||
|
||||
notificationsDebugStore = new NotificationsDebugStore(
|
||||
accountsStore,
|
||||
notificationsStore,
|
||||
pullRequestCoordinator
|
||||
)
|
||||
|
||||
appStore = new AppStore(
|
||||
githubUserStore,
|
||||
new CloningRepositoriesStore(),
|
||||
|
@ -117,6 +125,7 @@ describe('App', () => {
|
|||
issuesStore={issuesStore}
|
||||
gitHubUserStore={githubUserStore}
|
||||
aheadBehindStore={aheadBehindStore}
|
||||
notificationsDebugStore={notificationsDebugStore}
|
||||
startTime={0}
|
||||
/>
|
||||
) as unknown as React.Component<any, any>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"releases": {
|
||||
"3.1.7-beta1": ["[Improved] Upgrade embedded Git to 2.39.2"],
|
||||
"3.1.6": [
|
||||
"[Improved] Upgrade embedded Git to 2.39.1 and Git LFS to 3.3.0 - #15915"
|
||||
],
|
||||
"3.1.6-beta2": [
|
||||
"[Fixed] Fix crash launching the app on Apple silicon devices - #16011",
|
||||
"[Fixed] Trim leading and trailing whitespaces in URLs of repository remotes - #15821. Thanks @Shivareddy-Aluri!",
|
||||
|
|
|
@ -45,6 +45,7 @@ These editors are currently supported:
|
|||
- [RStudio](https://rstudio.com/)
|
||||
- [Aptana Studio](http://www.aptana.com/)
|
||||
- [JetBrains Fleet](https://www.jetbrains.com/fleet/)
|
||||
- [JetBrains DataSpell](https://www.jetbrains.com/dataspell/)
|
||||
|
||||
These are defined in a list at the top of the file:
|
||||
|
||||
|
@ -272,6 +273,7 @@ These editors are currently supported:
|
|||
- [Emacs](https://www.gnu.org/software/emacs/)
|
||||
- [Lite XL](https://lite-xl.com/)
|
||||
- [JetBrains Fleet](https://www.jetbrains.com/fleet/)
|
||||
- [JetBrains DataSpell](https://www.jetbrains.com/dataspell/)
|
||||
|
||||
These are defined in a list at the top of the file:
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
"jest-diff": "^25.0.0",
|
||||
"jest-extended": "^0.11.2",
|
||||
"jest-localstorage-mock": "^2.3.0",
|
||||
"jszip": "^3.7.1",
|
||||
"jszip": "^3.8.0",
|
||||
"klaw-sync": "^3.0.0",
|
||||
"legal-eagle": "0.16.0",
|
||||
"mini-css-extract-plugin": "^2.5.3",
|
||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -5245,9 +5245,9 @@ htmlparser2@^6.1.0:
|
|||
entities "^2.0.0"
|
||||
|
||||
http-cache-semantics@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
|
||||
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
|
||||
|
||||
http-errors@1.8.1:
|
||||
version "1.8.1"
|
||||
|
@ -6558,10 +6558,10 @@ jsx-ast-utils@^3.3.2:
|
|||
array-includes "^3.1.5"
|
||||
object.assign "^4.1.3"
|
||||
|
||||
jszip@^3.7.1:
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
|
||||
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
|
||||
jszip@^3.8.0:
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.8.0.tgz#a2ac3c33fe96a76489765168213655850254d51b"
|
||||
integrity sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw==
|
||||
dependencies:
|
||||
lie "~3.3.0"
|
||||
pako "~1.0.2"
|
||||
|
|
Loading…
Reference in a new issue