Merge pull request #16096 from desktop/test-notifications-tool

Add new tool to easily test notifications from real data
This commit is contained in:
Sergio Padrino 2023-02-14 09:07:55 +01:00 committed by GitHub
commit fa4b31bc7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1031 additions and 6 deletions

View file

@ -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.
*/

View file

@ -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)
}
}

View 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(),
})
}
}

View file

@ -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

View file

@ -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

View file

@ -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}`)
}

View file

@ -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')!

View 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()
}
)
}
}

View file

@ -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

View 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);
}
}

View file

@ -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>