mirror of
https://github.com/desktop/desktop
synced 2024-10-30 06:03:10 +00:00
Merge branch 'development' into releases/3.1.7-beta1
This commit is contained in:
commit
ccddcaa46e
31 changed files with 1456 additions and 225 deletions
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -32,19 +32,7 @@ jobs:
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
cache: yarn
|
||||||
- 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-
|
|
||||||
|
|
||||||
# This step can be removed as soon as official Windows arm64 builds are published:
|
# This step can be removed as soon as official Windows arm64 builds are published:
|
||||||
# https://github.com/nodejs/build/issues/2450#issuecomment-705853342
|
# https://github.com/nodejs/build/issues/2450#issuecomment-705853342
|
||||||
|
|
|
@ -508,6 +508,15 @@ export interface IAPIPullRequestReview {
|
||||||
| 'CHANGES_REQUESTED'
|
| '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. */
|
/** The metadata about a GitHub server. */
|
||||||
export interface IServerMetadata {
|
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.
|
* Get the combined status for the given ref.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -71,6 +71,10 @@ const editors: IDarwinExternalEditor[] = [
|
||||||
name: 'PyCharm Community Edition',
|
name: 'PyCharm Community Edition',
|
||||||
bundleIdentifiers: ['com.jetbrains.pycharm.ce'],
|
bundleIdentifiers: ['com.jetbrains.pycharm.ce'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'DataSpell',
|
||||||
|
bundleIdentifiers: ['com.jetbrains.DataSpell'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'RubyMine',
|
name: 'RubyMine',
|
||||||
bundleIdentifiers: ['com.jetbrains.RubyMine'],
|
bundleIdentifiers: ['com.jetbrains.RubyMine'],
|
||||||
|
|
|
@ -23,9 +23,26 @@ const editors: ILinuxExternalEditor[] = [
|
||||||
name: 'Neovim',
|
name: 'Neovim',
|
||||||
paths: ['/usr/bin/nvim'],
|
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',
|
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)',
|
name: 'Visual Studio Code (Insiders)',
|
||||||
|
@ -33,7 +50,11 @@ const editors: ILinuxExternalEditor[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'VSCodium',
|
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',
|
name: 'Sublime Text',
|
||||||
|
@ -63,17 +84,69 @@ const editors: ILinuxExternalEditor[] = [
|
||||||
paths: ['/usr/bin/lite-xl'],
|
paths: ['/usr/bin/lite-xl'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Jetbrains PhpStorm',
|
name: 'JetBrains PhpStorm',
|
||||||
paths: ['/snap/bin/phpstorm'],
|
paths: [
|
||||||
|
'/snap/bin/phpstorm',
|
||||||
|
'.local/share/JetBrains/Toolbox/scripts/phpstorm',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Jetbrains WebStorm',
|
name: 'JetBrains WebStorm',
|
||||||
paths: ['/snap/bin/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',
|
name: 'Emacs',
|
||||||
paths: ['/snap/bin/emacs', '/usr/local/bin/emacs', '/usr/bin/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> {
|
async function getAvailablePath(paths: string[]): Promise<string | null> {
|
||||||
|
|
|
@ -471,6 +471,14 @@ const editors: WindowsExternalEditor[] = [
|
||||||
displayNamePrefix: 'Fleet ',
|
displayNamePrefix: 'Fleet ',
|
||||||
publishers: ['JetBrains s.r.o.'],
|
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(
|
function getKeyOrEmpty(
|
||||||
|
|
|
@ -28,13 +28,23 @@ export interface IDesktopPullRequestReviewSubmitAliveEvent {
|
||||||
readonly pull_request_number: number
|
readonly pull_request_number: number
|
||||||
readonly state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED'
|
readonly state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED'
|
||||||
readonly review_id: string
|
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. */
|
/** Represents an Alive event relevant to Desktop. */
|
||||||
export type DesktopAliveEvent =
|
export type DesktopAliveEvent =
|
||||||
| IDesktopChecksFailedAliveEvent
|
| IDesktopChecksFailedAliveEvent
|
||||||
| IDesktopPullRequestReviewSubmitAliveEvent
|
| IDesktopPullRequestReviewSubmitAliveEvent
|
||||||
|
| IDesktopPullRequestCommentAliveEvent
|
||||||
interface IAliveSubscription {
|
interface IAliveSubscription {
|
||||||
readonly account: Account
|
readonly account: Account
|
||||||
readonly subscription: Subscription<AliveStore>
|
readonly subscription: Subscription<AliveStore>
|
||||||
|
@ -247,7 +257,11 @@ export class AliveStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = event.data as any as DesktopAliveEvent
|
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)
|
this.emitter.emit(this.ALIVE_EVENT_RECEIVED_EVENT, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7300,8 +7300,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
private onPullRequestReviewSubmitNotification = async (
|
private onPullRequestReviewSubmitNotification = async (
|
||||||
repository: RepositoryWithGitHubRepository,
|
repository: RepositoryWithGitHubRepository,
|
||||||
pullRequest: PullRequest,
|
pullRequest: PullRequest,
|
||||||
review: ValidNotificationPullRequestReview,
|
review: ValidNotificationPullRequestReview
|
||||||
numberOfComments: number
|
|
||||||
) => {
|
) => {
|
||||||
const selectedRepository =
|
const selectedRepository =
|
||||||
this.selectedRepository ?? (await this._selectRepository(repository))
|
this.selectedRepository ?? (await this._selectRepository(repository))
|
||||||
|
@ -7322,7 +7321,6 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
review,
|
review,
|
||||||
pullRequest,
|
pullRequest,
|
||||||
repository,
|
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 = (
|
type OnPullRequestReviewSubmitCallback = (
|
||||||
repository: RepositoryWithGitHubRepository,
|
repository: RepositoryWithGitHubRepository,
|
||||||
pullRequest: PullRequest,
|
pullRequest: PullRequest,
|
||||||
review: ValidNotificationPullRequestReview,
|
review: ValidNotificationPullRequestReview
|
||||||
numberOfComments: number
|
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,6 +104,12 @@ export class NotificationsStore {
|
||||||
public onNotificationEventReceived: NotificationCallback<DesktopAliveEvent> =
|
public onNotificationEventReceived: NotificationCallback<DesktopAliveEvent> =
|
||||||
async (event, id, userInfo) => this.handleAliveEvent(userInfo, true)
|
async (event, id, userInfo) => this.handleAliveEvent(userInfo, true)
|
||||||
|
|
||||||
|
public simulateAliveEvent(event: DesktopAliveEvent) {
|
||||||
|
if (__DEV__) {
|
||||||
|
this.handleAliveEvent(event, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async handleAliveEvent(
|
private async handleAliveEvent(
|
||||||
e: DesktopAliveEvent,
|
e: DesktopAliveEvent,
|
||||||
skipNotification: boolean
|
skipNotification: boolean
|
||||||
|
@ -175,12 +180,7 @@ export class NotificationsStore {
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
this.statsStore.recordPullRequestReviewNotificationClicked(review.state)
|
this.statsStore.recordPullRequestReviewNotificationClicked(review.state)
|
||||||
|
|
||||||
this.onPullRequestReviewSubmitCallback?.(
|
this.onPullRequestReviewSubmitCallback?.(repository, pullRequest, review)
|
||||||
repository,
|
|
||||||
pullRequest,
|
|
||||||
review,
|
|
||||||
event.number_of_comments
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipNotification) {
|
if (skipNotification) {
|
||||||
|
|
|
@ -729,7 +729,13 @@ function createWindow() {
|
||||||
electron: '>=1.2.1',
|
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) {
|
for (const extension of extensions) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -89,6 +89,7 @@ export enum PopupType {
|
||||||
StartPullRequest = 'StartPullRequest',
|
StartPullRequest = 'StartPullRequest',
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
InstallingUpdate = 'InstallingUpdate',
|
InstallingUpdate = 'InstallingUpdate',
|
||||||
|
TestNotifications = 'TestNotifications',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IBasePopup {
|
interface IBasePopup {
|
||||||
|
@ -361,7 +362,6 @@ export type PopupDetail =
|
||||||
repository: RepositoryWithGitHubRepository
|
repository: RepositoryWithGitHubRepository
|
||||||
pullRequest: PullRequest
|
pullRequest: PullRequest
|
||||||
review: ValidNotificationPullRequestReview
|
review: ValidNotificationPullRequestReview
|
||||||
numberOfComments: number
|
|
||||||
shouldCheckoutBranch: boolean
|
shouldCheckoutBranch: boolean
|
||||||
shouldChangeRepository: boolean
|
shouldChangeRepository: boolean
|
||||||
}
|
}
|
||||||
|
@ -389,5 +389,9 @@ export type PopupDetail =
|
||||||
| {
|
| {
|
||||||
type: PopupType.InstallingUpdate
|
type: PopupType.InstallingUpdate
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: PopupType.TestNotifications
|
||||||
|
repository: RepositoryWithGitHubRepository
|
||||||
|
}
|
||||||
|
|
||||||
export type Popup = IBasePopup & PopupDetail
|
export type Popup = IBasePopup & PopupDetail
|
||||||
|
|
|
@ -148,7 +148,6 @@ import { WarnForcePushDialog } from './multi-commit-operation/dialog/warn-force-
|
||||||
import { clamp } from '../lib/clamp'
|
import { clamp } from '../lib/clamp'
|
||||||
import { generateRepositoryListContextMenu } from './repositories-list/repository-list-item-context-menu'
|
import { generateRepositoryListContextMenu } from './repositories-list/repository-list-item-context-menu'
|
||||||
import * as ipcRenderer from '../lib/ipc-renderer'
|
import * as ipcRenderer from '../lib/ipc-renderer'
|
||||||
import { showNotification } from '../lib/notifications/show-notification'
|
|
||||||
import { DiscardChangesRetryDialog } from './discard-changes/discard-changes-retry-dialog'
|
import { DiscardChangesRetryDialog } from './discard-changes/discard-changes-retry-dialog'
|
||||||
import { generateDevReleaseSummary } from '../lib/release-notes'
|
import { generateDevReleaseSummary } from '../lib/release-notes'
|
||||||
import { PullRequestReview } from './notifications/pull-request-review'
|
import { PullRequestReview } from './notifications/pull-request-review'
|
||||||
|
@ -164,6 +163,8 @@ import { uuid } from '../lib/uuid'
|
||||||
import { InstallingUpdate } from './installing-update/installing-update'
|
import { InstallingUpdate } from './installing-update/installing-update'
|
||||||
import { enableStackedPopups } from '../lib/feature-flag'
|
import { enableStackedPopups } from '../lib/feature-flag'
|
||||||
import { DialogStackContext } from './dialog'
|
import { DialogStackContext } from './dialog'
|
||||||
|
import { TestNotifications } from './test-notifications/test-notifications'
|
||||||
|
import { NotificationsDebugStore } from '../lib/stores/notifications-debug-store'
|
||||||
|
|
||||||
const MinuteInMilliseconds = 1000 * 60
|
const MinuteInMilliseconds = 1000 * 60
|
||||||
const HourInMilliseconds = MinuteInMilliseconds * 60
|
const HourInMilliseconds = MinuteInMilliseconds * 60
|
||||||
|
@ -185,6 +186,7 @@ interface IAppProps {
|
||||||
readonly issuesStore: IssuesStore
|
readonly issuesStore: IssuesStore
|
||||||
readonly gitHubUserStore: GitHubUserStore
|
readonly gitHubUserStore: GitHubUserStore
|
||||||
readonly aheadBehindStore: AheadBehindStore
|
readonly aheadBehindStore: AheadBehindStore
|
||||||
|
readonly notificationsDebugStore: NotificationsDebugStore
|
||||||
readonly startTime: number
|
readonly startTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -489,10 +491,19 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotification({
|
// if current repository is not repository with github repository, return
|
||||||
title: 'Test notification',
|
const repository = this.getRepository()
|
||||||
body: 'Click here! This is a test notification',
|
if (
|
||||||
onClick: () => this.props.dispatcher.showPopup({ type: PopupType.About }),
|
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: {
|
case PopupType.PullRequestReview: {
|
||||||
return (
|
return (
|
||||||
<PullRequestReview
|
<PullRequestReview
|
||||||
key="pull-request-checks-failed"
|
key="pull-request-review"
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
shouldCheckoutBranch={popup.shouldCheckoutBranch}
|
shouldCheckoutBranch={popup.shouldCheckoutBranch}
|
||||||
shouldChangeRepository={popup.shouldChangeRepository}
|
shouldChangeRepository={popup.shouldChangeRepository}
|
||||||
repository={popup.repository}
|
repository={popup.repository}
|
||||||
pullRequest={popup.pullRequest}
|
pullRequest={popup.pullRequest}
|
||||||
review={popup.review}
|
review={popup.review}
|
||||||
numberOfComments={popup.numberOfComments}
|
|
||||||
emoji={this.state.emoji}
|
emoji={this.state.emoji}
|
||||||
accounts={this.state.accounts}
|
accounts={this.state.accounts}
|
||||||
onSubmit={onPopupDismissedFn}
|
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:
|
default:
|
||||||
return assertNever(popup, `Unknown popup type: ${popup}`)
|
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 React from 'react'
|
||||||
import { Select } from '../lib/select'
|
import { Select } from '../lib/select'
|
||||||
import { Button } from '../lib/button'
|
import { Button } from '../lib/button'
|
||||||
|
@ -73,10 +71,17 @@ export class CommitMessageAvatar extends React.Component<
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<div className="commit-message-avatar-component">
|
<div className="commit-message-avatar-component">
|
||||||
<div onClick={this.onAvatarClick}>
|
{this.props.warningBadgeVisible && (
|
||||||
{this.props.warningBadgeVisible && this.renderWarningBadge()}
|
<Button className="avatar-button" onClick={this.onAvatarClick}>
|
||||||
|
{this.renderWarningBadge()}
|
||||||
<Avatar user={this.props.user} title={this.props.title} />
|
<Avatar user={this.props.user} title={this.props.title} />
|
||||||
</div>
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!this.props.warningBadgeVisible && (
|
||||||
|
<Avatar user={this.props.user} title={this.props.title} />
|
||||||
|
)}
|
||||||
|
|
||||||
{this.state.isPopoverOpen && this.renderPopover()}
|
{this.state.isPopoverOpen && this.renderPopover()}
|
||||||
</div>
|
</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) {
|
if (this.props.warningBadgeVisible === false) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -383,7 +383,7 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
|
||||||
* 4. Any remaining button
|
* 4. Any remaining button
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
private focusFirstSuitableChild() {
|
public focusFirstSuitableChild() {
|
||||||
const dialog = this.dialogElement
|
const dialog = this.dialogElement
|
||||||
|
|
||||||
if (dialog === null) {
|
if (dialog === null) {
|
||||||
|
|
|
@ -536,6 +536,7 @@ export class CommitSummary extends React.Component<
|
||||||
let filesAdded = 0
|
let filesAdded = 0
|
||||||
let filesModified = 0
|
let filesModified = 0
|
||||||
let filesRemoved = 0
|
let filesRemoved = 0
|
||||||
|
let filesRenamed = 0
|
||||||
for (const file of this.props.changesetData.files) {
|
for (const file of this.props.changesetData.files) {
|
||||||
switch (file.status.kind) {
|
switch (file.status.kind) {
|
||||||
case AppFileStatusKind.New:
|
case AppFileStatusKind.New:
|
||||||
|
@ -547,9 +548,14 @@ export class CommitSummary extends React.Component<
|
||||||
case AppFileStatusKind.Deleted:
|
case AppFileStatusKind.Deleted:
|
||||||
filesRemoved += 1
|
filesRemoved += 1
|
||||||
break
|
break
|
||||||
|
case AppFileStatusKind.Renamed:
|
||||||
|
filesRenamed += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasFileDescription =
|
||||||
|
filesAdded + filesModified + filesRemoved + filesRenamed > 0
|
||||||
|
|
||||||
const filesLongDescription = (
|
const filesLongDescription = (
|
||||||
<>
|
<>
|
||||||
{filesAdded > 0 ? (
|
{filesAdded > 0 ? (
|
||||||
|
@ -579,6 +585,15 @@ export class CommitSummary extends React.Component<
|
||||||
{filesRemoved} deleted
|
{filesRemoved} deleted
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : 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
|
<TooltippedContent
|
||||||
className="commit-summary-meta-item without-truncation"
|
className="commit-summary-meta-item without-truncation"
|
||||||
tooltipClassName="changed-files-description-tooltip"
|
tooltipClassName="changed-files-description-tooltip"
|
||||||
tooltip={fileCount > 0 ? filesLongDescription : undefined}
|
tooltip={
|
||||||
|
fileCount > 0 && hasFileDescription ? filesLongDescription : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Octicon symbol={OcticonSymbol.diff} />
|
<Octicon symbol={OcticonSymbol.diff} />
|
||||||
{filesShortDescription}
|
{filesShortDescription}
|
||||||
|
|
|
@ -79,6 +79,7 @@ import { NotificationsStore } from '../lib/stores/notifications-store'
|
||||||
import * as ipcRenderer from '../lib/ipc-renderer'
|
import * as ipcRenderer from '../lib/ipc-renderer'
|
||||||
import { migrateRendererGUID } from '../lib/get-renderer-guid'
|
import { migrateRendererGUID } from '../lib/get-renderer-guid'
|
||||||
import { initializeRendererNotificationHandler } from '../lib/notifications/notification-handler'
|
import { initializeRendererNotificationHandler } from '../lib/notifications/notification-handler'
|
||||||
|
import { NotificationsDebugStore } from '../lib/stores/notifications-debug-store'
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
installDevGlobals()
|
installDevGlobals()
|
||||||
|
@ -267,6 +268,12 @@ const notificationsStore = new NotificationsStore(
|
||||||
statsStore
|
statsStore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const notificationsDebugStore = new NotificationsDebugStore(
|
||||||
|
accountsStore,
|
||||||
|
notificationsStore,
|
||||||
|
pullRequestCoordinator
|
||||||
|
)
|
||||||
|
|
||||||
const appStore = new AppStore(
|
const appStore = new AppStore(
|
||||||
gitHubUserStore,
|
gitHubUserStore,
|
||||||
cloningRepositoriesStore,
|
cloningRepositoriesStore,
|
||||||
|
@ -353,6 +360,7 @@ ReactDOM.render(
|
||||||
issuesStore={issuesStore}
|
issuesStore={issuesStore}
|
||||||
gitHubUserStore={gitHubUserStore}
|
gitHubUserStore={gitHubUserStore}
|
||||||
aheadBehindStore={aheadBehindStore}
|
aheadBehindStore={aheadBehindStore}
|
||||||
|
notificationsDebugStore={notificationsDebugStore}
|
||||||
startTime={startTime}
|
startTime={startTime}
|
||||||
/>,
|
/>,
|
||||||
document.getElementById('desktop-app-container')!
|
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 * as React from 'react'
|
||||||
import { Dialog, DialogContent, DialogFooter } from '../dialog'
|
|
||||||
import { Row } from '../lib/row'
|
import { Row } from '../lib/row'
|
||||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||||
import { PullRequest } from '../../models/pull-request'
|
import { PullRequest } from '../../models/pull-request'
|
||||||
import { Dispatcher } from '../dispatcher'
|
import { Dispatcher } from '../dispatcher'
|
||||||
import { Account } from '../../models/account'
|
import { Account } from '../../models/account'
|
||||||
import { Octicon } from '../octicons'
|
|
||||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
|
||||||
import { RepositoryWithGitHubRepository } from '../../models/repository'
|
import { RepositoryWithGitHubRepository } from '../../models/repository'
|
||||||
import { SandboxedMarkdown } from '../lib/sandboxed-markdown'
|
|
||||||
import {
|
import {
|
||||||
getPullRequestReviewStateIcon,
|
getPullRequestReviewStateIcon,
|
||||||
getVerbForPullRequestReview,
|
getVerbForPullRequestReview,
|
||||||
} from './pull-request-review-helpers'
|
} from './pull-request-review-helpers'
|
||||||
import { LinkButton } from '../lib/link-button'
|
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 { ValidNotificationPullRequestReview } from '../../lib/valid-notification-pull-request-review'
|
||||||
import { getStealthEmailForUser } from '../../lib/email'
|
import { PullRequestCommentLike } from './pull-request-comment-like'
|
||||||
|
|
||||||
interface IPullRequestReviewProps {
|
interface IPullRequestReviewProps {
|
||||||
readonly dispatcher: Dispatcher
|
readonly dispatcher: Dispatcher
|
||||||
|
@ -26,7 +19,6 @@ interface IPullRequestReviewProps {
|
||||||
readonly repository: RepositoryWithGitHubRepository
|
readonly repository: RepositoryWithGitHubRepository
|
||||||
readonly pullRequest: PullRequest
|
readonly pullRequest: PullRequest
|
||||||
readonly review: ValidNotificationPullRequestReview
|
readonly review: ValidNotificationPullRequestReview
|
||||||
readonly numberOfComments: number
|
|
||||||
|
|
||||||
/** Map from the emoji shortcut (e.g., :+1:) to the image's local path. */
|
/** Map from the emoji shortcut (e.g., :+1:) to the image's local path. */
|
||||||
readonly emoji: Map<string, string>
|
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<
|
export class PullRequestReview extends React.Component<
|
||||||
IPullRequestReviewProps,
|
IPullRequestReviewProps,
|
||||||
|
@ -62,115 +54,42 @@ export class PullRequestReview extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { title, pullRequestNumber } = this.props.pullRequest
|
const {
|
||||||
|
dispatcher,
|
||||||
|
accounts,
|
||||||
|
repository,
|
||||||
|
pullRequest,
|
||||||
|
emoji,
|
||||||
|
review,
|
||||||
|
onSubmit,
|
||||||
|
onDismissed,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
const header = (
|
const icon = getPullRequestReviewStateIcon(review.state)
|
||||||
<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>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<PullRequestCommentLike
|
||||||
id="pull-request-review"
|
dispatcher={dispatcher}
|
||||||
type="normal"
|
accounts={accounts}
|
||||||
title={header}
|
repository={repository}
|
||||||
dismissable={false}
|
pullRequest={pullRequest}
|
||||||
onSubmit={this.props.onSubmit}
|
emoji={emoji}
|
||||||
onDismissed={this.props.onDismissed}
|
eventDate={new Date(review.submitted_at)}
|
||||||
loading={this.state.switchingToPullRequest}
|
eventVerb={getVerbForPullRequestReview(review)}
|
||||||
>
|
eventIconSymbol={icon.symbol}
|
||||||
<DialogContent>
|
eventIconClass={icon.className}
|
||||||
<div className="review-container">
|
externalURL={review.html_url}
|
||||||
{this.renderTimelineItem()}
|
user={review.user}
|
||||||
{this.renderCommentBubble()}
|
body={review.body}
|
||||||
</div>
|
switchingToPullRequest={this.state.switchingToPullRequest}
|
||||||
</DialogContent>
|
renderFooterContent={this.renderFooterContent}
|
||||||
<DialogFooter>{this.renderFooterContent()}</DialogFooter>
|
onSubmit={onSubmit}
|
||||||
</Dialog>
|
onDismissed={onDismissed}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderTimelineItem() {
|
private renderFooterContent = () => {
|
||||||
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() {
|
|
||||||
const { review, shouldChangeRepository, shouldCheckoutBranch } = this.props
|
const { review, shouldChangeRepository, shouldCheckoutBranch } = this.props
|
||||||
const isApprovedReview = review.state === 'APPROVED'
|
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>) => {
|
private onSubmit = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,8 @@ const SignInWithBrowserTitle = __DARWIN__
|
||||||
const DefaultTitle = 'Sign in'
|
const DefaultTitle = 'Sign in'
|
||||||
|
|
||||||
export class SignIn extends React.Component<ISignInProps, ISignInState> {
|
export class SignIn extends React.Component<ISignInProps, ISignInState> {
|
||||||
|
private readonly dialogRef = React.createRef<Dialog>()
|
||||||
|
|
||||||
public constructor(props: ISignInProps) {
|
public constructor(props: ISignInProps) {
|
||||||
super(props)
|
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) {
|
public componentWillReceiveProps(nextProps: ISignInProps) {
|
||||||
if (nextProps.signInState !== this.props.signInState) {
|
if (nextProps.signInState !== this.props.signInState) {
|
||||||
if (
|
if (
|
||||||
|
@ -161,6 +175,7 @@ export class SignIn extends React.Component<ISignInProps, ISignInState> {
|
||||||
<OkCancelButtonGroup
|
<OkCancelButtonGroup
|
||||||
okButtonText={primaryButtonText}
|
okButtonText={primaryButtonText}
|
||||||
okButtonDisabled={disableSubmit}
|
okButtonDisabled={disableSubmit}
|
||||||
|
onCancelButtonClick={this.onDismissed}
|
||||||
/>
|
/>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
)
|
)
|
||||||
|
@ -324,6 +339,7 @@ export class SignIn extends React.Component<ISignInProps, ISignInState> {
|
||||||
onDismissed={this.onDismissed}
|
onDismissed={this.onDismissed}
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
loading={state.loading}
|
loading={state.loading}
|
||||||
|
ref={this.dialogRef}
|
||||||
>
|
>
|
||||||
{errors}
|
{errors}
|
||||||
{this.renderStep()}
|
{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-popover';
|
||||||
@import 'ui/check-runs/ci-check-run-job-steps';
|
@import 'ui/check-runs/ci-check-run-job-steps';
|
||||||
@import 'ui/_pull-request-checks-failed';
|
@import 'ui/_pull-request-checks-failed';
|
||||||
@import 'ui/_pull-request-review';
|
|
||||||
@import 'ui/_sandboxed-markdown';
|
@import 'ui/_sandboxed-markdown';
|
||||||
@import 'ui/_pull-request-quick-view';
|
@import 'ui/_pull-request-quick-view';
|
||||||
@import 'ui/discard-changes-retry';
|
@import 'ui/discard-changes-retry';
|
||||||
|
|
|
@ -2,6 +2,27 @@
|
||||||
// With this, the popover's absolute position will be relative to its parent
|
// With this, the popover's absolute position will be relative to its parent
|
||||||
position: relative;
|
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 {
|
.warning-badge {
|
||||||
background-color: var(--commit-warning-badge-background-color);
|
background-color: var(--commit-warning-badge-background-color);
|
||||||
border: var(--commit-warning-badge-border-color) 1px solid;
|
border: var(--commit-warning-badge-border-color) 1px solid;
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
@import 'dialogs/unreachable-commits';
|
@import 'dialogs/unreachable-commits';
|
||||||
@import 'dialogs/open-pull-request';
|
@import 'dialogs/open-pull-request';
|
||||||
@import 'dialogs/installing-update';
|
@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
|
// 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
|
// 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;
|
--avatar-size: 40px;
|
||||||
min-width: 500px;
|
min-width: 500px;
|
||||||
|
|
||||||
|
@ -6,7 +7,7 @@
|
||||||
height: unset;
|
height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pull-request-review-dialog-header {
|
.pull-request-comment-like-dialog-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -44,7 +45,7 @@
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.review-container {
|
.comment-container {
|
||||||
.timeline-line {
|
.timeline-line {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
@ -103,7 +104,7 @@
|
||||||
.link-button-component {
|
.link-button-component {
|
||||||
color: unset;
|
color: unset;
|
||||||
|
|
||||||
&.reviewer {
|
&.author {
|
||||||
font-weight: bold;
|
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);
|
color: var(--color-deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.files-renamed-icon {
|
||||||
|
color: var(--color-renamed);
|
||||||
|
}
|
||||||
|
|
||||||
.octicon {
|
.octicon {
|
||||||
margin-right: var(--spacing-third);
|
margin-right: var(--spacing-third);
|
||||||
vertical-align: bottom; // For some reason, `bottom` places the text in the middle
|
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 { AheadBehindStore } from '../../src/lib/stores/ahead-behind-store'
|
||||||
import { AliveStore } from '../../src/lib/stores/alive-store'
|
import { AliveStore } from '../../src/lib/stores/alive-store'
|
||||||
import { NotificationsStore } from '../../src/lib/stores/notifications-store'
|
import { NotificationsStore } from '../../src/lib/stores/notifications-store'
|
||||||
|
import { NotificationsDebugStore } from '../../src/lib/stores/notifications-debug-store'
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
let appStore: AppStore
|
let appStore: AppStore
|
||||||
|
@ -41,6 +42,7 @@ describe('App', () => {
|
||||||
let githubUserStore: GitHubUserStore
|
let githubUserStore: GitHubUserStore
|
||||||
let issuesStore: IssuesStore
|
let issuesStore: IssuesStore
|
||||||
let aheadBehindStore: AheadBehindStore
|
let aheadBehindStore: AheadBehindStore
|
||||||
|
let notificationsDebugStore: NotificationsDebugStore
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const db = new TestGitHubUserDatabase()
|
const db = new TestGitHubUserDatabase()
|
||||||
|
@ -86,6 +88,12 @@ describe('App', () => {
|
||||||
)
|
)
|
||||||
notificationsStore.setNotificationsEnabled(false)
|
notificationsStore.setNotificationsEnabled(false)
|
||||||
|
|
||||||
|
notificationsDebugStore = new NotificationsDebugStore(
|
||||||
|
accountsStore,
|
||||||
|
notificationsStore,
|
||||||
|
pullRequestCoordinator
|
||||||
|
)
|
||||||
|
|
||||||
appStore = new AppStore(
|
appStore = new AppStore(
|
||||||
githubUserStore,
|
githubUserStore,
|
||||||
new CloningRepositoriesStore(),
|
new CloningRepositoriesStore(),
|
||||||
|
@ -117,6 +125,7 @@ describe('App', () => {
|
||||||
issuesStore={issuesStore}
|
issuesStore={issuesStore}
|
||||||
gitHubUserStore={githubUserStore}
|
gitHubUserStore={githubUserStore}
|
||||||
aheadBehindStore={aheadBehindStore}
|
aheadBehindStore={aheadBehindStore}
|
||||||
|
notificationsDebugStore={notificationsDebugStore}
|
||||||
startTime={0}
|
startTime={0}
|
||||||
/>
|
/>
|
||||||
) as unknown as React.Component<any, any>
|
) as unknown as React.Component<any, any>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
{
|
{
|
||||||
"releases": {
|
"releases": {
|
||||||
"3.1.7-beta1": ["[Improved] Upgrade embedded Git to 2.39.2"],
|
"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": [
|
"3.1.6-beta2": [
|
||||||
"[Fixed] Fix crash launching the app on Apple silicon devices - #16011",
|
"[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!",
|
"[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/)
|
- [RStudio](https://rstudio.com/)
|
||||||
- [Aptana Studio](http://www.aptana.com/)
|
- [Aptana Studio](http://www.aptana.com/)
|
||||||
- [JetBrains Fleet](https://www.jetbrains.com/fleet/)
|
- [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:
|
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/)
|
- [Emacs](https://www.gnu.org/software/emacs/)
|
||||||
- [Lite XL](https://lite-xl.com/)
|
- [Lite XL](https://lite-xl.com/)
|
||||||
- [JetBrains Fleet](https://www.jetbrains.com/fleet/)
|
- [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:
|
These are defined in a list at the top of the file:
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
"jest-diff": "^25.0.0",
|
"jest-diff": "^25.0.0",
|
||||||
"jest-extended": "^0.11.2",
|
"jest-extended": "^0.11.2",
|
||||||
"jest-localstorage-mock": "^2.3.0",
|
"jest-localstorage-mock": "^2.3.0",
|
||||||
"jszip": "^3.7.1",
|
"jszip": "^3.8.0",
|
||||||
"klaw-sync": "^3.0.0",
|
"klaw-sync": "^3.0.0",
|
||||||
"legal-eagle": "0.16.0",
|
"legal-eagle": "0.16.0",
|
||||||
"mini-css-extract-plugin": "^2.5.3",
|
"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"
|
entities "^2.0.0"
|
||||||
|
|
||||||
http-cache-semantics@^4.0.0:
|
http-cache-semantics@^4.0.0:
|
||||||
version "4.1.0"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
|
||||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
|
||||||
|
|
||||||
http-errors@1.8.1:
|
http-errors@1.8.1:
|
||||||
version "1.8.1"
|
version "1.8.1"
|
||||||
|
@ -6558,10 +6558,10 @@ jsx-ast-utils@^3.3.2:
|
||||||
array-includes "^3.1.5"
|
array-includes "^3.1.5"
|
||||||
object.assign "^4.1.3"
|
object.assign "^4.1.3"
|
||||||
|
|
||||||
jszip@^3.7.1:
|
jszip@^3.8.0:
|
||||||
version "3.7.1"
|
version "3.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
|
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.8.0.tgz#a2ac3c33fe96a76489765168213655850254d51b"
|
||||||
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
|
integrity sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw==
|
||||||
dependencies:
|
dependencies:
|
||||||
lie "~3.3.0"
|
lie "~3.3.0"
|
||||||
pako "~1.0.2"
|
pako "~1.0.2"
|
||||||
|
|
Loading…
Reference in a new issue