mirror of
https://github.com/desktop/desktop
synced 2024-09-19 08:02:22 +00:00
Merge pull request #16096 from desktop/test-notifications-tool
Add new tool to easily test notifications from real data
This commit is contained in:
commit
fa4b31bc7e
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -30,10 +30,21 @@ export interface IDesktopPullRequestReviewSubmitAliveEvent {
|
|||
readonly review_id: string
|
||||
}
|
||||
|
||||
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>
|
||||
|
@ -246,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)
|
||||
}
|
||||
}
|
||||
|
|
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(),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -104,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
|
||||
|
|
|
@ -89,6 +89,7 @@ export enum PopupType {
|
|||
StartPullRequest = 'StartPullRequest',
|
||||
Error = 'Error',
|
||||
InstallingUpdate = 'InstallingUpdate',
|
||||
TestNotifications = 'TestNotifications',
|
||||
}
|
||||
|
||||
interface IBasePopup {
|
||||
|
@ -388,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,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2373,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}`)
|
||||
}
|
||||
|
|
|
@ -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')!
|
||||
|
|
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@
|
|||
@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
|
||||
|
|
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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue