@kitlangton/ghui 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/package.json +2 -1
- package/src/App.tsx +983 -238
- package/src/domain.ts +77 -10
- package/src/services/CommandRunner.ts +8 -1
- package/src/services/GitHubService.ts +291 -193
- package/src/ui/DetailsPane.tsx +26 -25
- package/src/ui/FooterHints.tsx +62 -6
- package/src/ui/PullRequestDiffPane.tsx +160 -49
- package/src/ui/PullRequestList.tsx +28 -10
- package/src/ui/colors.ts +41 -0
- package/src/ui/commentEditor.ts +126 -0
- package/src/ui/diff.ts +229 -16
- package/src/ui/modals.tsx +236 -9
- package/src/ui/primitives.tsx +2 -2
package/src/App.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ScrollBoxRenderable } from "@opentui/core"
|
|
1
|
+
import type { DiffRenderable, ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
3
3
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
4
4
|
import { Cause, Effect, Layer, Schedule } from "effect"
|
|
@@ -6,18 +6,19 @@ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
|
6
6
|
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
7
7
|
import { useEffect, useMemo, useRef, useState } from "react"
|
|
8
8
|
import { config } from "./config.js"
|
|
9
|
-
import type
|
|
9
|
+
import { pullRequestQueueLabels, pullRequestQueueModes, type CreatePullRequestCommentInput, type DiffCommentSide, type LoadStatus, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestQueueMode, type PullRequestReviewComment } from "./domain.js"
|
|
10
10
|
import { formatShortDate, formatTimestamp } from "./date.js"
|
|
11
11
|
import { availableMergeActions, mergeInfoFromPullRequest } from "./mergeActions.js"
|
|
12
12
|
import { Observability } from "./observability.js"
|
|
13
13
|
import { GitHubService } from "./services/GitHubService.js"
|
|
14
14
|
import { loadStoredThemeId, saveStoredThemeId } from "./themeStore.js"
|
|
15
15
|
import { colors, filterThemeDefinitions, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
16
|
-
import {
|
|
16
|
+
import { backspace as editorBackspace, deleteForward as editorDeleteForward, deleteToLineEnd, deleteToLineStart, deleteWordBackward, deleteWordForward, insertText, moveLeft as editorMoveLeft, moveLineEnd, moveLineStart, moveRight as editorMoveRight, moveVertically, moveWordBackward, moveWordForward, type CommentEditorValue } from "./ui/commentEditor.js"
|
|
17
|
+
import { buildStackedDiffFiles, diffCommentAnchorKey, diffCommentLocationKey, getStackedDiffCommentAnchors, nearestDiffCommentAnchorIndex, PullRequestDiffState, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles, stackedDiffFileAtLine, type DiffCommentAnchor, type DiffView, type DiffWrapMode, type StackedDiffCommentAnchor } from "./ui/diff.js"
|
|
17
18
|
import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailBodyHeight, getDetailHeaderHeight, getDetailJunctionRows, getDetailsPaneHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
18
|
-
import { FooterHints,
|
|
19
|
+
import { FooterHints, initialRetryProgress, RetryProgress } from "./ui/FooterHints.js"
|
|
19
20
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
20
|
-
import { CloseModal, initialCloseModalState, initialLabelModalState, initialMergeModalState, initialThemeModalState, LabelModal, MergeModal, ThemeModal } from "./ui/modals.js"
|
|
21
|
+
import { CloseModal, CommentModal, CommentThreadModal, initialCloseModalState, initialCommentModalState, initialCommentThreadModalState, initialLabelModalState, initialMergeModalState, initialModal, initialThemeModalState, LabelModal, MergeModal, Modal, ThemeModal, type CloseModalState, type CommentModalState, type CommentThreadModalState, type LabelModalState, type MergeModalState, type ModalState, type ModalTag, type ThemeModalState } from "./ui/modals.js"
|
|
21
22
|
import { groupBy, reviewLabel } from "./ui/pullRequests.js"
|
|
22
23
|
import { PullRequestDiffPane } from "./ui/PullRequestDiffPane.js"
|
|
23
24
|
import { PullRequestList } from "./ui/PullRequestList.js"
|
|
@@ -25,45 +26,104 @@ import { PullRequestList } from "./ui/PullRequestList.js"
|
|
|
25
26
|
const githubRuntime = Atom.runtime(GitHubService.layer.pipe(Layer.provideMerge(Observability.layer)))
|
|
26
27
|
const initialThemeId = await Effect.runPromise(loadStoredThemeId)
|
|
27
28
|
|
|
28
|
-
type LoadStatus = "loading" | "ready" | "error"
|
|
29
29
|
|
|
30
30
|
interface PullRequestLoad {
|
|
31
|
+
readonly queueMode: PullRequestQueueMode
|
|
31
32
|
readonly data: readonly PullRequestItem[]
|
|
32
33
|
readonly fetchedAt: Date | null
|
|
34
|
+
readonly detailsFetchedAt: Date | null
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
interface DetailPlaceholderInput {
|
|
36
38
|
readonly status: LoadStatus
|
|
37
|
-
readonly retryProgress: RetryProgress
|
|
39
|
+
readonly retryProgress: RetryProgress
|
|
38
40
|
readonly loadingIndicator: string
|
|
39
41
|
readonly visibleCount: number
|
|
40
42
|
readonly filterText: string
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
type DiffLineColorConfig = {
|
|
46
|
+
readonly gutter: string
|
|
47
|
+
readonly content: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type DiffSideRenderable = {
|
|
51
|
+
readonly setLineColor: (line: number, color: DiffLineColorConfig) => void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type DiffRenderableRuntimeSides = {
|
|
55
|
+
readonly leftSide?: DiffSideRenderable
|
|
56
|
+
readonly rightSide?: DiffSideRenderable
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface AppliedDiffLineColor {
|
|
60
|
+
readonly anchor: StackedDiffCommentAnchor
|
|
61
|
+
readonly view: DiffView
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface AppliedDiffLineColorState {
|
|
65
|
+
readonly contextKey: string | null
|
|
66
|
+
readonly entries: readonly AppliedDiffLineColor[]
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
const PR_FETCH_RETRIES = 6
|
|
44
70
|
const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
45
71
|
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
46
72
|
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
73
|
+
const DIFF_STICKY_HEADER_LINES = 2
|
|
47
74
|
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
48
75
|
|
|
49
|
-
const
|
|
76
|
+
const mergeCachedDetails = (fresh: readonly PullRequestItem[], cached: readonly PullRequestItem[] | undefined) => {
|
|
77
|
+
if (!cached) return fresh
|
|
78
|
+
const cachedByUrl = new Map(cached.map((pullRequest) => [pullRequest.url, pullRequest]))
|
|
79
|
+
return fresh.map((pullRequest) => {
|
|
80
|
+
const cachedPullRequest = cachedByUrl.get(pullRequest.url)
|
|
81
|
+
if (!cachedPullRequest?.detailLoaded) return pullRequest
|
|
82
|
+
return {
|
|
83
|
+
...pullRequest,
|
|
84
|
+
body: cachedPullRequest.body,
|
|
85
|
+
labels: cachedPullRequest.labels,
|
|
86
|
+
additions: cachedPullRequest.additions,
|
|
87
|
+
deletions: cachedPullRequest.deletions,
|
|
88
|
+
changedFiles: cachedPullRequest.changedFiles,
|
|
89
|
+
checkStatus: cachedPullRequest.checkStatus,
|
|
90
|
+
checkSummary: cachedPullRequest.checkSummary,
|
|
91
|
+
checks: cachedPullRequest.checks,
|
|
92
|
+
detailLoaded: true,
|
|
93
|
+
} satisfies PullRequestItem
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const retryProgressAtom = Atom.make<RetryProgress>(initialRetryProgress).pipe(Atom.keepAlive)
|
|
98
|
+
const queueModeAtom = Atom.make<PullRequestQueueMode>("authored").pipe(Atom.keepAlive)
|
|
99
|
+
const queueLoadCacheAtom = Atom.make<Partial<Record<PullRequestQueueMode, PullRequestLoad>>>({}).pipe(Atom.keepAlive)
|
|
100
|
+
const queueSelectionAtom = Atom.make<Partial<Record<PullRequestQueueMode, number>>>({}).pipe(Atom.keepAlive)
|
|
50
101
|
const pullRequestsAtom = githubRuntime.atom(
|
|
51
102
|
GitHubService.use((github) =>
|
|
52
103
|
Effect.gen(function*() {
|
|
53
|
-
yield* Atom.
|
|
54
|
-
|
|
104
|
+
const queueMode = yield* Atom.get(queueModeAtom)
|
|
105
|
+
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
106
|
+
const data = yield* github.listOpenPullRequests(queueMode).pipe(
|
|
55
107
|
Effect.tapError(() =>
|
|
56
|
-
Atom.update(retryProgressAtom, (current) => ({
|
|
57
|
-
attempt: Math.min((current
|
|
108
|
+
Atom.update(retryProgressAtom, (current) => RetryProgress.Retrying({
|
|
109
|
+
attempt: Math.min(RetryProgress.$match(current, { Idle: () => 0, Retrying: ({ attempt }) => attempt }) + 1, PR_FETCH_RETRIES),
|
|
58
110
|
max: PR_FETCH_RETRIES,
|
|
59
111
|
}))
|
|
60
112
|
),
|
|
61
113
|
Effect.retry({ times: PR_FETCH_RETRIES, schedule: Schedule.exponential("300 millis", 2) }),
|
|
62
|
-
Effect.tapError(() => Atom.set(retryProgressAtom,
|
|
114
|
+
Effect.tapError(() => Atom.set(retryProgressAtom, initialRetryProgress)),
|
|
63
115
|
)
|
|
64
116
|
|
|
65
|
-
yield* Atom.set(retryProgressAtom,
|
|
66
|
-
|
|
117
|
+
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
118
|
+
const cache = yield* Atom.get(queueLoadCacheAtom)
|
|
119
|
+
const load = {
|
|
120
|
+
queueMode,
|
|
121
|
+
data: mergeCachedDetails(data, cache[queueMode]?.data),
|
|
122
|
+
fetchedAt: new Date(),
|
|
123
|
+
detailsFetchedAt: null,
|
|
124
|
+
} satisfies PullRequestLoad
|
|
125
|
+
yield* Atom.set(queueLoadCacheAtom, { ...cache, [queueMode]: load })
|
|
126
|
+
return load
|
|
67
127
|
})
|
|
68
128
|
),
|
|
69
129
|
).pipe(Atom.keepAlive)
|
|
@@ -77,15 +137,17 @@ const detailFullViewAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
|
77
137
|
const detailScrollOffsetAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
78
138
|
const diffFullViewAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
79
139
|
const diffFileIndexAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
80
|
-
const
|
|
81
|
-
const
|
|
140
|
+
const diffScrollTopAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
141
|
+
const diffRenderViewAtom = Atom.make<DiffView>("split").pipe(Atom.keepAlive)
|
|
142
|
+
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none").pipe(Atom.keepAlive)
|
|
143
|
+
const diffCommentModeAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
144
|
+
const diffCommentAnchorIndexAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
145
|
+
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
146
|
+
const diffCommentsLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
82
147
|
const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>({}).pipe(Atom.keepAlive)
|
|
83
148
|
|
|
84
|
-
const
|
|
85
|
-
const closeModalAtom = Atom.make(initialCloseModalState).pipe(Atom.keepAlive)
|
|
86
|
-
const mergeModalAtom = Atom.make(initialMergeModalState).pipe(Atom.keepAlive)
|
|
149
|
+
const activeModalAtom = Atom.make<Modal>(initialModal).pipe(Atom.keepAlive)
|
|
87
150
|
const themeIdAtom = Atom.make<ThemeId>(initialThemeId).pipe(Atom.keepAlive)
|
|
88
|
-
const themeModalAtom = Atom.make(initialThemeModalState).pipe(Atom.keepAlive)
|
|
89
151
|
const labelCacheAtom = Atom.make<Record<string, readonly PullRequestLabel[]>>({}).pipe(Atom.keepAlive)
|
|
90
152
|
const pullRequestOverridesAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
91
153
|
const recentlyCompletedPullRequestsAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
@@ -98,8 +160,8 @@ const usernameAtom = githubRuntime.atom(
|
|
|
98
160
|
const listRepoLabelsAtom = githubRuntime.fn<string>()((repository) =>
|
|
99
161
|
GitHubService.use((github) => github.listRepoLabels(repository))
|
|
100
162
|
)
|
|
101
|
-
const listOpenPullRequestDetailsAtom = githubRuntime.fn<
|
|
102
|
-
GitHubService.use((github) => github.listOpenPullRequestDetails())
|
|
163
|
+
const listOpenPullRequestDetailsAtom = githubRuntime.fn<PullRequestQueueMode>()((queueMode) =>
|
|
164
|
+
GitHubService.use((github) => github.listOpenPullRequestDetails(queueMode))
|
|
103
165
|
)
|
|
104
166
|
const addPullRequestLabelAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly label: string }>()((input) =>
|
|
105
167
|
GitHubService.use((github) => github.addPullRequestLabel(input.repository, input.number, input.label))
|
|
@@ -113,6 +175,9 @@ const toggleDraftAtom = githubRuntime.fn<{ readonly repository: string; readonly
|
|
|
113
175
|
const getPullRequestDiffAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
114
176
|
GitHubService.use((github) => github.getPullRequestDiff(input.repository, input.number))
|
|
115
177
|
)
|
|
178
|
+
const listPullRequestCommentsAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
179
|
+
GitHubService.use((github) => github.listPullRequestComments(input.repository, input.number))
|
|
180
|
+
)
|
|
116
181
|
const getPullRequestMergeInfoAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
117
182
|
GitHubService.use((github) => github.getPullRequestMergeInfo(input.repository, input.number))
|
|
118
183
|
)
|
|
@@ -122,11 +187,29 @@ const mergePullRequestAtom = githubRuntime.fn<{ readonly repository: string; rea
|
|
|
122
187
|
const closePullRequestAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
123
188
|
GitHubService.use((github) => github.closePullRequest(input.repository, input.number))
|
|
124
189
|
)
|
|
190
|
+
const createPullRequestCommentAtom = githubRuntime.fn<CreatePullRequestCommentInput>()((input) => GitHubService.use((github) => github.createPullRequestComment(input)))
|
|
191
|
+
|
|
192
|
+
const centeredOffset = (outer: number, inner: number) => Math.floor((outer - inner) / 2)
|
|
125
193
|
|
|
126
194
|
const deleteLastWord = (value: string) => value.replace(/\s*\S+\s*$/, "")
|
|
127
195
|
|
|
128
196
|
const errorMessage = (error: unknown) => error instanceof Error ? error.message : String(error)
|
|
129
197
|
|
|
198
|
+
const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) => {
|
|
199
|
+
const normalized = query.trim().toLowerCase()
|
|
200
|
+
if (normalized.length === 0) return 0
|
|
201
|
+
const fields = [
|
|
202
|
+
pullRequest.title.toLowerCase(),
|
|
203
|
+
pullRequest.repository.toLowerCase(),
|
|
204
|
+
String(pullRequest.number),
|
|
205
|
+
]
|
|
206
|
+
const scores = fields.flatMap((field, index) => {
|
|
207
|
+
const matchIndex = field.indexOf(normalized)
|
|
208
|
+
return matchIndex >= 0 ? [index * 1000 + matchIndex] : []
|
|
209
|
+
})
|
|
210
|
+
return scores.length > 0 ? Math.min(...scores) : null
|
|
211
|
+
}
|
|
212
|
+
|
|
130
213
|
const clipboardCommands = (): readonly (readonly string[])[] => {
|
|
131
214
|
if (process.platform === "darwin") return [["pbcopy"]]
|
|
132
215
|
if (process.platform === "linux") {
|
|
@@ -207,6 +290,76 @@ const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => k
|
|
|
207
290
|
|
|
208
291
|
const isThemeKey = (key: { readonly name: string; readonly ctrl?: boolean; readonly meta?: boolean }) => !key.ctrl && !key.meta && key.name.toLowerCase() === "t"
|
|
209
292
|
|
|
293
|
+
const nextQueueMode = (mode: PullRequestQueueMode, delta: 1 | -1) => {
|
|
294
|
+
const index = pullRequestQueueModes.indexOf(mode)
|
|
295
|
+
return pullRequestQueueModes[(index + delta + pullRequestQueueModes.length) % pullRequestQueueModes.length]!
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const diffCommentThreadKey = (pullRequest: PullRequestItem, comment: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
299
|
+
`${pullRequestDiffKey(pullRequest)}:${diffCommentLocationKey(comment)}`
|
|
300
|
+
|
|
301
|
+
const groupDiffCommentThreads = (pullRequest: PullRequestItem, comments: readonly PullRequestReviewComment[]) => {
|
|
302
|
+
const threads: Record<string, PullRequestReviewComment[]> = {}
|
|
303
|
+
for (const comment of comments) {
|
|
304
|
+
const key = diffCommentThreadKey(pullRequest, comment)
|
|
305
|
+
const thread = threads[key]
|
|
306
|
+
if (thread) thread.push(comment)
|
|
307
|
+
else threads[key] = [comment]
|
|
308
|
+
}
|
|
309
|
+
return threads
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const isLocalDiffComment = (comment: PullRequestReviewComment) => comment.id.startsWith("local:")
|
|
313
|
+
|
|
314
|
+
const originalDiffLineColor = (anchor: DiffCommentAnchor): DiffLineColorConfig => {
|
|
315
|
+
if (anchor.kind === "addition") {
|
|
316
|
+
return { gutter: colors.diff.addedLineNumberBg, content: colors.diff.addedBg }
|
|
317
|
+
}
|
|
318
|
+
if (anchor.kind === "deletion") {
|
|
319
|
+
return { gutter: colors.diff.removedLineNumberBg, content: colors.diff.removedBg }
|
|
320
|
+
}
|
|
321
|
+
return { gutter: colors.diff.lineNumberBg, content: colors.diff.contextBg }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const mixHexColor = (color: string, base: string, amount: number) => {
|
|
325
|
+
const parse = (hex: string) => {
|
|
326
|
+
const normalized = hex.replace(/^#/, "")
|
|
327
|
+
if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return null
|
|
328
|
+
return {
|
|
329
|
+
r: Number.parseInt(normalized.slice(0, 2), 16),
|
|
330
|
+
g: Number.parseInt(normalized.slice(2, 4), 16),
|
|
331
|
+
b: Number.parseInt(normalized.slice(4, 6), 16),
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const left = parse(color)
|
|
335
|
+
const right = parse(base)
|
|
336
|
+
if (!left || !right) return color
|
|
337
|
+
const channel = (key: "r" | "g" | "b") => Math.round(left[key] * amount + right[key] * (1 - amount)).toString(16).padStart(2, "0")
|
|
338
|
+
return `#${channel("r")}${channel("g")}${channel("b")}`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const diffCommentGutterColor = (anchor: DiffCommentAnchor, kind: "selected" | "thread") => {
|
|
342
|
+
const accent = kind === "thread"
|
|
343
|
+
? colors.status.pending
|
|
344
|
+
: anchor.side === "RIGHT" ? colors.status.passing : colors.status.failing
|
|
345
|
+
return mixHexColor(accent, originalDiffLineColor(anchor).gutter, 0.45)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const diffSideTargets = (diff: DiffRenderable, anchor: DiffCommentAnchor, view: DiffView) => {
|
|
349
|
+
const withSides = diff as unknown as DiffRenderableRuntimeSides
|
|
350
|
+
if (view === "split") {
|
|
351
|
+
const target = anchor.side === "LEFT" ? withSides.leftSide : withSides.rightSide
|
|
352
|
+
return target ? [target] : []
|
|
353
|
+
}
|
|
354
|
+
return withSides.leftSide ? [withSides.leftSide] : []
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const setDiffCommentLineColor = (diff: DiffRenderable, entry: AppliedDiffLineColor, color: DiffLineColorConfig) => {
|
|
358
|
+
for (const target of diffSideTargets(diff, entry.anchor, entry.view)) {
|
|
359
|
+
target.setLineColor(entry.anchor.localRenderLine, color)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
210
363
|
const getDetailPlaceholderContent = ({
|
|
211
364
|
status,
|
|
212
365
|
retryProgress,
|
|
@@ -217,7 +370,7 @@ const getDetailPlaceholderContent = ({
|
|
|
217
370
|
if (status === "loading") {
|
|
218
371
|
return {
|
|
219
372
|
title: `${loadingIndicator} Loading pull requests`,
|
|
220
|
-
hint: retryProgress ? `Retry ${retryProgress.attempt}/${retryProgress.max}` : "Fetching latest open PRs",
|
|
373
|
+
hint: retryProgress._tag === "Retrying" ? `Retry ${retryProgress.attempt}/${retryProgress.max}` : "Fetching latest open PRs",
|
|
221
374
|
}
|
|
222
375
|
}
|
|
223
376
|
|
|
@@ -253,6 +406,9 @@ export const App = () => {
|
|
|
253
406
|
const { width, height } = useTerminalDimensions()
|
|
254
407
|
const pullRequestResult = useAtomValue(pullRequestsAtom)
|
|
255
408
|
const refreshPullRequestsAtom = useAtomRefresh(pullRequestsAtom)
|
|
409
|
+
const [queueMode, setQueueMode] = useAtom(queueModeAtom)
|
|
410
|
+
const [queueLoadCache, setQueueLoadCache] = useAtom(queueLoadCacheAtom)
|
|
411
|
+
const [queueSelection, setQueueSelection] = useAtom(queueSelectionAtom)
|
|
256
412
|
const [selectedIndex, setSelectedIndex] = useAtom(selectedIndexAtom)
|
|
257
413
|
const [notice, setNotice] = useAtom(noticeAtom)
|
|
258
414
|
const [filterQuery, setFilterQuery] = useAtom(filterQueryAtom)
|
|
@@ -263,14 +419,45 @@ export const App = () => {
|
|
|
263
419
|
const [_detailScrollOffset, setDetailScrollOffset] = useAtom(detailScrollOffsetAtom)
|
|
264
420
|
const [diffFullView, setDiffFullView] = useAtom(diffFullViewAtom)
|
|
265
421
|
const [diffFileIndex, setDiffFileIndex] = useAtom(diffFileIndexAtom)
|
|
422
|
+
const [diffScrollTop, setDiffScrollTop] = useAtom(diffScrollTopAtom)
|
|
266
423
|
const [diffRenderView, setDiffRenderView] = useAtom(diffRenderViewAtom)
|
|
267
424
|
const [diffWrapMode, setDiffWrapMode] = useAtom(diffWrapModeAtom)
|
|
425
|
+
const [diffCommentMode, setDiffCommentMode] = useAtom(diffCommentModeAtom)
|
|
426
|
+
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
|
|
427
|
+
const [diffCommentThreads, setDiffCommentThreads] = useAtom(diffCommentThreadsAtom)
|
|
428
|
+
const [diffCommentsLoaded, setDiffCommentsLoaded] = useAtom(diffCommentsLoadedAtom)
|
|
268
429
|
const [pullRequestDiffCache, setPullRequestDiffCache] = useAtom(pullRequestDiffCacheAtom)
|
|
269
|
-
const [
|
|
270
|
-
const [closeModal, setCloseModal] = useAtom(closeModalAtom)
|
|
271
|
-
const [mergeModal, setMergeModal] = useAtom(mergeModalAtom)
|
|
430
|
+
const [activeModal, setActiveModal] = useAtom(activeModalAtom)
|
|
272
431
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
273
|
-
const
|
|
432
|
+
const closeActiveModal = () => setActiveModal(initialModal)
|
|
433
|
+
const labelModalActive = Modal.$is("Label")(activeModal)
|
|
434
|
+
const closeModalActive = Modal.$is("Close")(activeModal)
|
|
435
|
+
const mergeModalActive = Modal.$is("Merge")(activeModal)
|
|
436
|
+
const commentModalActive = Modal.$is("Comment")(activeModal)
|
|
437
|
+
const commentThreadModalActive = Modal.$is("CommentThread")(activeModal)
|
|
438
|
+
const themeModalActive = Modal.$is("Theme")(activeModal)
|
|
439
|
+
const labelModal: LabelModalState = labelModalActive ? activeModal : initialLabelModalState
|
|
440
|
+
const closeModal: CloseModalState = closeModalActive ? activeModal : initialCloseModalState
|
|
441
|
+
const mergeModal: MergeModalState = mergeModalActive ? activeModal : initialMergeModalState
|
|
442
|
+
const commentModal: CommentModalState = commentModalActive ? activeModal : initialCommentModalState
|
|
443
|
+
const commentThreadModal: CommentThreadModalState = commentThreadModalActive ? activeModal : initialCommentThreadModalState
|
|
444
|
+
const themeModal: ThemeModalState = themeModalActive ? activeModal : initialThemeModalState
|
|
445
|
+
const makeModalSetter = <Tag extends Exclude<ModalTag, "None">>(tag: Tag) =>
|
|
446
|
+
(next: ModalState<Tag> | ((prev: ModalState<Tag>) => ModalState<Tag>)) => setActiveModal((current) => {
|
|
447
|
+
const ctor = Modal[tag] as unknown as (args: ModalState<Tag>) => Modal
|
|
448
|
+
if (typeof next === "function") {
|
|
449
|
+
const updater = next as (prev: ModalState<Tag>) => ModalState<Tag>
|
|
450
|
+
if (current._tag !== tag) return current
|
|
451
|
+
return ctor(updater(current as unknown as ModalState<Tag>))
|
|
452
|
+
}
|
|
453
|
+
return ctor(next)
|
|
454
|
+
})
|
|
455
|
+
const setLabelModal = makeModalSetter("Label")
|
|
456
|
+
const setCloseModal = makeModalSetter("Close")
|
|
457
|
+
const setMergeModal = makeModalSetter("Merge")
|
|
458
|
+
const setCommentModal = makeModalSetter("Comment")
|
|
459
|
+
const setCommentThreadModal = makeModalSetter("CommentThread")
|
|
460
|
+
const setThemeModal = makeModalSetter("Theme")
|
|
274
461
|
setActiveTheme(themeId)
|
|
275
462
|
const themeIdRef = useRef(themeId)
|
|
276
463
|
const themeModalRef = useRef(themeModal)
|
|
@@ -291,9 +478,11 @@ export const App = () => {
|
|
|
291
478
|
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
292
479
|
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
293
480
|
const getPullRequestDiff = useAtomSet(getPullRequestDiffAtom, { mode: "promise" })
|
|
481
|
+
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
294
482
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
295
483
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
296
484
|
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
485
|
+
const createPullRequestComment = useAtomSet(createPullRequestCommentAtom, { mode: "promise" })
|
|
297
486
|
const terminalWidth = width ?? 100
|
|
298
487
|
const terminalHeight = height ?? 24
|
|
299
488
|
const contentWidth = Math.max(1, terminalWidth)
|
|
@@ -312,6 +501,7 @@ export const App = () => {
|
|
|
312
501
|
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
313
502
|
const detailHydrationRef = useRef<number | null>(null)
|
|
314
503
|
const refreshGenerationRef = useRef(0)
|
|
504
|
+
const didMountQueueModeRef = useRef(false)
|
|
315
505
|
const lastPullRequestRefreshAtRef = useRef(0)
|
|
316
506
|
const terminalFocusedRef = useRef(true)
|
|
317
507
|
const terminalWasBlurredRef = useRef(false)
|
|
@@ -320,6 +510,9 @@ export const App = () => {
|
|
|
320
510
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
321
511
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
322
512
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
513
|
+
const diffRenderableRefs = useRef(new Map<number, DiffRenderable>())
|
|
514
|
+
const diffCommentLineColorsRef = useRef<AppliedDiffLineColorState>({ contextKey: null, entries: [] })
|
|
515
|
+
const suppressNextDiffCommentScrollRef = useRef(false)
|
|
323
516
|
const headerFooterWidth = Math.max(24, contentWidth - 2)
|
|
324
517
|
|
|
325
518
|
const flashNotice = (message: string) => {
|
|
@@ -348,7 +541,9 @@ export const App = () => {
|
|
|
348
541
|
}
|
|
349
542
|
}, [])
|
|
350
543
|
|
|
351
|
-
const
|
|
544
|
+
const resolvedPullRequestLoad = AsyncResult.getOrElse(pullRequestResult, () => null)
|
|
545
|
+
const pullRequestLoad = queueLoadCache[queueMode] ?? (resolvedPullRequestLoad?.queueMode === queueMode ? resolvedPullRequestLoad : null)
|
|
546
|
+
const isLoadingQueueMode = resolvedPullRequestLoad !== null && resolvedPullRequestLoad.queueMode !== queueMode
|
|
352
547
|
const pullRequests = useMemo(() => {
|
|
353
548
|
const source = pullRequestLoad?.data ?? []
|
|
354
549
|
const seenUrls = new Set<string>()
|
|
@@ -362,7 +557,7 @@ export const App = () => {
|
|
|
362
557
|
...Object.values(recentlyCompletedPullRequests).filter((pullRequest) => !seenUrls.has(pullRequest.url)),
|
|
363
558
|
]
|
|
364
559
|
}, [pullRequestLoad?.data, pullRequestOverrides, recentlyCompletedPullRequests])
|
|
365
|
-
const pullRequestStatus: LoadStatus = pullRequestResult.waiting && pullRequestLoad === null
|
|
560
|
+
const pullRequestStatus: LoadStatus = (pullRequestResult.waiting || isLoadingQueueMode) && pullRequestLoad === null
|
|
366
561
|
? "loading"
|
|
367
562
|
: AsyncResult.isFailure(pullRequestResult)
|
|
368
563
|
? "error"
|
|
@@ -375,18 +570,42 @@ export const App = () => {
|
|
|
375
570
|
const effectiveFilterQuery = (filterMode ? filterDraft : filterQuery).trim().toLowerCase()
|
|
376
571
|
const visibleFilterText = filterMode ? filterDraft : filterQuery
|
|
377
572
|
|
|
378
|
-
const filteredPullRequests = useMemo(() =>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
573
|
+
const filteredPullRequests = useMemo(() => {
|
|
574
|
+
if (effectiveFilterQuery.length === 0) return pullRequests
|
|
575
|
+
return pullRequests.flatMap((pullRequest) => {
|
|
576
|
+
const score = pullRequestFilterScore(pullRequest, effectiveFilterQuery)
|
|
577
|
+
return score === null ? [] : [{ pullRequest, score }]
|
|
578
|
+
}).sort((left, right) =>
|
|
579
|
+
left.score - right.score || right.pullRequest.createdAt.getTime() - left.pullRequest.createdAt.getTime()
|
|
580
|
+
).map(({ pullRequest }) => pullRequest)
|
|
581
|
+
}, [pullRequests, effectiveFilterQuery])
|
|
582
|
+
|
|
583
|
+
const visibleRepoOrder = useMemo(() => effectiveFilterQuery.length > 0
|
|
584
|
+
? [...new Set(filteredPullRequests.map((pullRequest) => pullRequest.repository))]
|
|
585
|
+
: [], [filteredPullRequests, effectiveFilterQuery.length])
|
|
385
586
|
const visibleGroups = useMemo(
|
|
386
|
-
() => groupBy(filteredPullRequests, (pullRequest) => pullRequest.repository),
|
|
387
|
-
[filteredPullRequests],
|
|
587
|
+
() => groupBy(filteredPullRequests, (pullRequest) => pullRequest.repository, visibleRepoOrder),
|
|
588
|
+
[filteredPullRequests, visibleRepoOrder],
|
|
388
589
|
)
|
|
389
590
|
const visiblePullRequests = useMemo(() => visibleGroups.flatMap(([, pullRequests]) => pullRequests), [visibleGroups])
|
|
591
|
+
const selectedPullRequest = visiblePullRequests[selectedIndex] ?? null
|
|
592
|
+
const selectedDiffState = selectedPullRequest ? pullRequestDiffCache[pullRequestDiffKey(selectedPullRequest)] : undefined
|
|
593
|
+
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
594
|
+
const readyDiffFiles = selectedDiffState?._tag === "Ready" ? selectedDiffState.files : []
|
|
595
|
+
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
596
|
+
const selectedDiffKey = selectedPullRequest ? pullRequestDiffKey(selectedPullRequest) : null
|
|
597
|
+
const diffCommentAnchors = useMemo(
|
|
598
|
+
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
599
|
+
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
|
|
600
|
+
)
|
|
601
|
+
const selectedDiffCommentAnchor = diffCommentAnchors[Math.max(0, Math.min(diffCommentAnchorIndex, diffCommentAnchors.length - 1))] ?? null
|
|
602
|
+
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? `${selectedDiffKey}:${diffCommentAnchorKey(selectedDiffCommentAnchor)}` : null
|
|
603
|
+
const selectedDiffCommentThread = selectedDiffCommentThreadKey ? diffCommentThreads[selectedDiffCommentThreadKey] ?? [] : []
|
|
604
|
+
const diffLineColorContextKey = selectedDiffKey ? `${selectedDiffKey}:${effectiveDiffRenderView}:${diffWrapMode}` : null
|
|
605
|
+
const diffCommentRows = useMemo(
|
|
606
|
+
() => [...new Set(diffCommentAnchors.map((anchor) => anchor.renderLine))].sort((left, right) => left - right),
|
|
607
|
+
[diffCommentAnchors],
|
|
608
|
+
)
|
|
390
609
|
const groupStarts = useMemo(() => visibleGroups.reduce<Array<number>>((starts, [, pullRequests], index) => {
|
|
391
610
|
if (index === 0) {
|
|
392
611
|
starts.push(0)
|
|
@@ -406,12 +625,15 @@ export const App = () => {
|
|
|
406
625
|
: pullRequestStatus === "loading"
|
|
407
626
|
? "loading pull requests..."
|
|
408
627
|
: ""
|
|
409
|
-
const headerLeft = username ? `GHUI ${username}` :
|
|
628
|
+
const headerLeft = username ? `GHUI ${username} ${pullRequestQueueLabels[queueMode]}` : `GHUI ${pullRequestQueueLabels[queueMode]}`
|
|
410
629
|
const headerLine = `${fitCell(headerLeft, Math.max(0, headerFooterWidth - summaryRight.length))}${summaryRight}`
|
|
411
630
|
const footerNotice = notice ? fitCell(notice, headerFooterWidth) : null
|
|
412
631
|
const selectPullRequestByUrl = (url: string) => {
|
|
413
632
|
const index = visiblePullRequests.findIndex((pullRequest) => pullRequest.url === url)
|
|
414
|
-
if (index >= 0)
|
|
633
|
+
if (index >= 0) {
|
|
634
|
+
setSelectedIndex(index)
|
|
635
|
+
setQueueSelection((current) => ({ ...current, [queueMode]: index }))
|
|
636
|
+
}
|
|
415
637
|
}
|
|
416
638
|
const updatePullRequest = (url: string, transform: (pullRequest: PullRequestItem) => PullRequestItem) => {
|
|
417
639
|
const pullRequest = pullRequests.find((item) => item.url === url)
|
|
@@ -429,6 +651,23 @@ export const App = () => {
|
|
|
429
651
|
refreshPullRequestsAtom()
|
|
430
652
|
}
|
|
431
653
|
refreshPullRequestsRef.current = refreshPullRequests
|
|
654
|
+
const switchQueueMode = (delta: 1 | -1) => {
|
|
655
|
+
const mode = nextQueueMode(queueMode, delta)
|
|
656
|
+
if (mode === queueMode) return
|
|
657
|
+
refreshGenerationRef.current += 1
|
|
658
|
+
setQueueSelection((current) => ({ ...current, [queueMode]: selectedIndex }))
|
|
659
|
+
setQueueMode(mode)
|
|
660
|
+
setSelectedIndex(queueSelection[mode] ?? 0)
|
|
661
|
+
setRecentlyCompletedPullRequests({})
|
|
662
|
+
detailHydrationRef.current = null
|
|
663
|
+
setDetailFullView(false)
|
|
664
|
+
setDiffFullView(false)
|
|
665
|
+
setDiffCommentMode(false)
|
|
666
|
+
setFilterDraft(filterQuery)
|
|
667
|
+
setNotice(null)
|
|
668
|
+
setRefreshCompletionMessage(null)
|
|
669
|
+
setRefreshStartedAt(null)
|
|
670
|
+
}
|
|
432
671
|
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
433
672
|
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
434
673
|
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
@@ -443,6 +682,15 @@ export const App = () => {
|
|
|
443
682
|
}
|
|
444
683
|
}, [pullRequestLoad?.fetchedAt])
|
|
445
684
|
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
if (!didMountQueueModeRef.current) {
|
|
687
|
+
didMountQueueModeRef.current = true
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
if (queueLoadCache[queueMode]) return
|
|
691
|
+
refreshPullRequestsAtom()
|
|
692
|
+
}, [queueMode, queueLoadCache, refreshPullRequestsAtom])
|
|
693
|
+
|
|
446
694
|
useEffect(() => {
|
|
447
695
|
if (!refreshCompletionMessage || refreshStartedAt === null) return
|
|
448
696
|
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
@@ -498,16 +746,73 @@ export const App = () => {
|
|
|
498
746
|
})
|
|
499
747
|
}, [visiblePullRequests.length])
|
|
500
748
|
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
setQueueSelection((current) => current[queueMode] === selectedIndex ? current : { ...current, [queueMode]: selectedIndex })
|
|
751
|
+
}, [queueMode, selectedIndex])
|
|
752
|
+
|
|
501
753
|
useEffect(() => {
|
|
502
754
|
setDiffFileIndex(0)
|
|
755
|
+
setDiffScrollTop(0)
|
|
756
|
+
setDiffCommentAnchorIndex(0)
|
|
503
757
|
}, [selectedIndex])
|
|
504
758
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
759
|
+
useEffect(() => {
|
|
760
|
+
setDiffCommentAnchorIndex((current) => {
|
|
761
|
+
if (diffCommentAnchors.length === 0) return 0
|
|
762
|
+
return Math.max(0, Math.min(current, diffCommentAnchors.length - 1))
|
|
763
|
+
})
|
|
764
|
+
}, [diffCommentAnchors.length])
|
|
765
|
+
|
|
766
|
+
useEffect(() => {
|
|
767
|
+
if (!diffCommentMode || !selectedDiffCommentAnchor) return
|
|
768
|
+
setDiffFileIndex((current) => current === selectedDiffCommentAnchor.fileIndex ? current : selectedDiffCommentAnchor.fileIndex)
|
|
769
|
+
}, [diffCommentMode, selectedDiffCommentAnchor?.fileIndex])
|
|
770
|
+
|
|
771
|
+
useEffect(() => {
|
|
772
|
+
const previous = diffCommentLineColorsRef.current
|
|
773
|
+
if (previous.contextKey === diffLineColorContextKey) {
|
|
774
|
+
for (const entry of previous.entries) {
|
|
775
|
+
const diff = diffRenderableRefs.current.get(entry.anchor.fileIndex)
|
|
776
|
+
if (diff) setDiffCommentLineColor(diff, entry, originalDiffLineColor(entry.anchor))
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const nextEntries: AppliedDiffLineColor[] = []
|
|
781
|
+
const appliedKeys = new Set<string>()
|
|
782
|
+
const applyLineColor = (anchor: StackedDiffCommentAnchor, gutter: string, override = false) => {
|
|
783
|
+
const key = `${effectiveDiffRenderView}:${anchor.side}:${anchor.renderLine}`
|
|
784
|
+
if (appliedKeys.has(key) && !override) return
|
|
785
|
+
appliedKeys.add(key)
|
|
786
|
+
const entry = { anchor, view: effectiveDiffRenderView } satisfies AppliedDiffLineColor
|
|
787
|
+
const diff = diffRenderableRefs.current.get(anchor.fileIndex)
|
|
788
|
+
if (diff) setDiffCommentLineColor(diff, entry, { ...originalDiffLineColor(anchor), gutter })
|
|
789
|
+
if (!nextEntries.some((existing) => existing.view === entry.view && existing.anchor.side === anchor.side && existing.anchor.renderLine === anchor.renderLine)) {
|
|
790
|
+
nextEntries.push(entry)
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (selectedDiffKey) {
|
|
795
|
+
for (const anchor of diffCommentAnchors) {
|
|
796
|
+
if ((diffCommentThreads[`${selectedDiffKey}:${diffCommentAnchorKey(anchor)}`]?.length ?? 0) > 0) {
|
|
797
|
+
applyLineColor(anchor, diffCommentGutterColor(anchor, "thread"))
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (diffCommentMode && selectedDiffCommentAnchor) {
|
|
802
|
+
applyLineColor(selectedDiffCommentAnchor, diffCommentGutterColor(selectedDiffCommentAnchor, "selected"), true)
|
|
803
|
+
if (suppressNextDiffCommentScrollRef.current) {
|
|
804
|
+
suppressNextDiffCommentScrollRef.current = false
|
|
805
|
+
} else {
|
|
806
|
+
ensureDiffLineVisible(selectedDiffCommentAnchor.renderLine)
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
suppressNextDiffCommentScrollRef.current = false
|
|
810
|
+
}
|
|
811
|
+
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
812
|
+
}, [diffCommentMode, selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, diffLineColorContextKey, effectiveDiffRenderView, diffCommentAnchors, diffCommentThreads])
|
|
508
813
|
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && pullRequests.some((pullRequest) => pullRequest.state === "open" && !pullRequest.detailLoaded)
|
|
509
814
|
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
510
|
-
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?.
|
|
815
|
+
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
511
816
|
const loadingIndicator = LOADING_FRAMES[loadingFrame % LOADING_FRAMES.length]!
|
|
512
817
|
|
|
513
818
|
useEffect(() => {
|
|
@@ -521,23 +826,29 @@ export const App = () => {
|
|
|
521
826
|
useEffect(() => {
|
|
522
827
|
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
523
828
|
if (pullRequestStatus !== "ready" || fetchedAt === undefined) return
|
|
524
|
-
if (detailHydrationRef.current === fetchedAt) return
|
|
829
|
+
if (detailHydrationRef.current === fetchedAt || pullRequestLoad?.detailsFetchedAt?.getTime() === fetchedAt) return
|
|
525
830
|
if (!pullRequests.some((pullRequest) => pullRequest.state === "open" && !pullRequest.detailLoaded)) return
|
|
526
831
|
detailHydrationRef.current = fetchedAt
|
|
527
832
|
const generation = refreshGenerationRef.current
|
|
528
|
-
void loadPullRequestDetails().then((details) => {
|
|
833
|
+
void loadPullRequestDetails(queueMode).then((details) => {
|
|
529
834
|
if (generation !== refreshGenerationRef.current) return
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
835
|
+
setQueueLoadCache((current) => {
|
|
836
|
+
const load = current[queueMode]
|
|
837
|
+
if (!load) return current
|
|
838
|
+
const detailsByUrl = new Map(details.map((detail) => [detail.url, detail]))
|
|
839
|
+
return {
|
|
840
|
+
...current,
|
|
841
|
+
[queueMode]: {
|
|
842
|
+
...load,
|
|
843
|
+
data: load.data.map((pullRequest) => detailsByUrl.get(pullRequest.url) ?? pullRequest),
|
|
844
|
+
detailsFetchedAt: load.fetchedAt,
|
|
845
|
+
},
|
|
534
846
|
}
|
|
535
|
-
return next
|
|
536
847
|
})
|
|
537
848
|
}).catch((error) => {
|
|
538
|
-
flashNotice(
|
|
849
|
+
flashNotice(errorMessage(error))
|
|
539
850
|
})
|
|
540
|
-
}, [pullRequestStatus, pullRequestLoad?.fetchedAt, pullRequests.length])
|
|
851
|
+
}, [queueMode, pullRequestStatus, pullRequestLoad?.fetchedAt, pullRequests.length])
|
|
541
852
|
|
|
542
853
|
const detailPlaceholderContent = getDetailPlaceholderContent({
|
|
543
854
|
status: pullRequestStatus,
|
|
@@ -550,23 +861,67 @@ export const App = () => {
|
|
|
550
861
|
|
|
551
862
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
552
863
|
|
|
553
|
-
const
|
|
864
|
+
const loadPullRequestComments = (pullRequest: PullRequestItem, force = false) => {
|
|
865
|
+
const key = pullRequestDiffKey(pullRequest)
|
|
866
|
+
const previousLoadState = diffCommentsLoaded[key]
|
|
867
|
+
if (!force && previousLoadState) return
|
|
868
|
+
setDiffCommentsLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
869
|
+
void listPullRequestComments({ repository: pullRequest.repository, number: pullRequest.number })
|
|
870
|
+
.then((comments) => {
|
|
871
|
+
setDiffCommentsLoaded((current) => ({ ...current, [key]: "ready" }))
|
|
872
|
+
setDiffCommentThreads((current) => {
|
|
873
|
+
const prefix = `${key}:`
|
|
874
|
+
const threads = groupDiffCommentThreads(pullRequest, comments)
|
|
875
|
+
const next: Record<string, readonly PullRequestReviewComment[]> = Object.fromEntries(
|
|
876
|
+
Object.entries(current).filter(([threadKey]) => !threadKey.startsWith(prefix)),
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
for (const [threadKey, threadComments] of Object.entries(current)) {
|
|
880
|
+
if (!threadKey.startsWith(prefix)) continue
|
|
881
|
+
const localComments = threadComments.filter(isLocalDiffComment)
|
|
882
|
+
if (localComments.length > 0) {
|
|
883
|
+
next[threadKey] = [...(threads[threadKey] ?? []), ...localComments]
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
for (const [threadKey, threadComments] of Object.entries(threads)) {
|
|
888
|
+
if (!next[threadKey]) next[threadKey] = threadComments
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return next
|
|
892
|
+
})
|
|
893
|
+
})
|
|
894
|
+
.catch((error) => {
|
|
895
|
+
setDiffCommentsLoaded((current) => {
|
|
896
|
+
if (previousLoadState === "ready") return { ...current, [key]: previousLoadState }
|
|
897
|
+
const next = { ...current }
|
|
898
|
+
delete next[key]
|
|
899
|
+
return next
|
|
900
|
+
})
|
|
901
|
+
flashNotice(errorMessage(error))
|
|
902
|
+
})
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const loadPullRequestDiff = (pullRequest: PullRequestItem, options: { readonly force?: boolean; readonly includeComments?: boolean } = {}) => {
|
|
906
|
+
const force = options.force ?? false
|
|
907
|
+
const includeComments = options.includeComments ?? false
|
|
554
908
|
const key = pullRequestDiffKey(pullRequest)
|
|
555
909
|
const existing = pullRequestDiffCache[key]
|
|
556
|
-
if (
|
|
910
|
+
if (includeComments) loadPullRequestComments(pullRequest, force)
|
|
911
|
+
if (!force && existing && (existing._tag === "Ready" || existing._tag === "Loading")) return
|
|
557
912
|
|
|
558
|
-
setPullRequestDiffCache((current) => ({ ...current, [key]:
|
|
913
|
+
setPullRequestDiffCache((current) => ({ ...current, [key]: PullRequestDiffState.Loading() }))
|
|
559
914
|
void getPullRequestDiff({ repository: pullRequest.repository, number: pullRequest.number })
|
|
560
915
|
.then((patch) => {
|
|
561
916
|
setPullRequestDiffCache((current) => ({
|
|
562
917
|
...current,
|
|
563
|
-
[key]: {
|
|
918
|
+
[key]: PullRequestDiffState.Ready({ patch, files: splitPatchFiles(patch) }),
|
|
564
919
|
}))
|
|
565
920
|
})
|
|
566
921
|
.catch((error) => {
|
|
567
922
|
setPullRequestDiffCache((current) => ({
|
|
568
923
|
...current,
|
|
569
|
-
[key]: {
|
|
924
|
+
[key]: PullRequestDiffState.Error({ error: errorMessage(error) }),
|
|
570
925
|
}))
|
|
571
926
|
flashNotice(errorMessage(error))
|
|
572
927
|
})
|
|
@@ -590,12 +945,219 @@ export const App = () => {
|
|
|
590
945
|
|
|
591
946
|
const openDiffView = () => {
|
|
592
947
|
if (!selectedPullRequest) return
|
|
948
|
+
diffRenderableRefs.current.clear()
|
|
949
|
+
diffCommentLineColorsRef.current = { contextKey: null, entries: [] }
|
|
593
950
|
setDiffFullView(true)
|
|
594
951
|
setDetailFullView(false)
|
|
952
|
+
setDiffCommentMode(false)
|
|
595
953
|
setDiffFileIndex(0)
|
|
954
|
+
setDiffScrollTop(0)
|
|
596
955
|
setDiffRenderView(contentWidth >= 100 ? "split" : "unified")
|
|
597
956
|
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
598
|
-
loadPullRequestDiff(selectedPullRequest)
|
|
957
|
+
loadPullRequestDiff(selectedPullRequest, { includeComments: true })
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const setDiffRenderableRef = (index: number, diff: DiffRenderable | null) => {
|
|
961
|
+
if (diff) diffRenderableRefs.current.set(index, diff)
|
|
962
|
+
else diffRenderableRefs.current.delete(index)
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const scrollToDiffFile = (index: number) => {
|
|
966
|
+
const stackedFile = stackedDiffFiles[index]
|
|
967
|
+
diffScrollRef.current?.scrollTo({ x: 0, y: stackedFile?.headerLine ?? 0 })
|
|
968
|
+
syncDiffScrollState()
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const syncDiffScrollState = () => {
|
|
972
|
+
const scrollTop = diffScrollRef.current?.scrollTop
|
|
973
|
+
if (scrollTop === undefined || stackedDiffFiles.length === 0) return
|
|
974
|
+
setDiffScrollTop((current) => current === scrollTop ? current : scrollTop)
|
|
975
|
+
const nextIndex = stackedDiffFileAtLine(stackedDiffFiles, scrollTop)?.index ?? 0
|
|
976
|
+
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const scrollDiffBy = (y: number) => {
|
|
980
|
+
diffScrollRef.current?.scrollBy({ x: 0, y })
|
|
981
|
+
syncDiffScrollState()
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const scrollDiffTo = (y: number) => {
|
|
985
|
+
diffScrollRef.current?.scrollTo({ x: 0, y })
|
|
986
|
+
syncDiffScrollState()
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const clearPendingGTimeout = () => {
|
|
990
|
+
if (pendingGTimeoutRef.current !== null) {
|
|
991
|
+
clearTimeout(pendingGTimeoutRef.current)
|
|
992
|
+
pendingGTimeoutRef.current = null
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const handleVimGoto = (key: { readonly name: string; readonly shift?: boolean }, gotoStart: () => void, gotoEnd: () => void): boolean => {
|
|
997
|
+
if (isShiftG(key)) {
|
|
998
|
+
gotoEnd()
|
|
999
|
+
setPendingG(false)
|
|
1000
|
+
clearPendingGTimeout()
|
|
1001
|
+
return true
|
|
1002
|
+
}
|
|
1003
|
+
if (key.name === "g") {
|
|
1004
|
+
if (pendingG) {
|
|
1005
|
+
gotoStart()
|
|
1006
|
+
setPendingG(false)
|
|
1007
|
+
clearPendingGTimeout()
|
|
1008
|
+
} else {
|
|
1009
|
+
setPendingG(true)
|
|
1010
|
+
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1011
|
+
setPendingG(false)
|
|
1012
|
+
pendingGTimeoutRef.current = null
|
|
1013
|
+
}, 500)
|
|
1014
|
+
}
|
|
1015
|
+
return true
|
|
1016
|
+
}
|
|
1017
|
+
return false
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const ensureDiffLineVisible = (line: number) => {
|
|
1021
|
+
const scroll = diffScrollRef.current
|
|
1022
|
+
if (!scroll) return
|
|
1023
|
+
const viewportHeight = Math.max(1, wideBodyHeight - (selectedDiffCommentThread.length > 0 ? 6 : 3))
|
|
1024
|
+
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, line, DIFF_STICKY_HEADER_LINES)
|
|
1025
|
+
if (nextTop !== scroll.scrollTop) {
|
|
1026
|
+
scroll.scrollTo({ x: 0, y: nextTop })
|
|
1027
|
+
syncDiffScrollState()
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
useEffect(() => {
|
|
1032
|
+
if (!diffFullView) return
|
|
1033
|
+
const interval = globalThis.setInterval(syncDiffScrollState, 80)
|
|
1034
|
+
return () => globalThis.clearInterval(interval)
|
|
1035
|
+
}, [diffFullView, stackedDiffFiles])
|
|
1036
|
+
|
|
1037
|
+
const jumpDiffFile = (delta: 1 | -1) => {
|
|
1038
|
+
if (readyDiffFiles.length === 0) return
|
|
1039
|
+
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
1040
|
+
setDiffFileIndex(nextIndex)
|
|
1041
|
+
if (diffCommentMode) {
|
|
1042
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex && anchor.side === selectedDiffCommentAnchor?.side)
|
|
1043
|
+
?? diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex)
|
|
1044
|
+
if (nextAnchor) setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1045
|
+
}
|
|
1046
|
+
scrollToDiffFile(nextIndex)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const enterDiffCommentMode = () => {
|
|
1050
|
+
const scrollTop = diffScrollRef.current?.scrollTop ?? 0
|
|
1051
|
+
suppressNextDiffCommentScrollRef.current = true
|
|
1052
|
+
setDiffCommentAnchorIndex(nearestDiffCommentAnchorIndex(diffCommentAnchors, scrollTop + DIFF_STICKY_HEADER_LINES))
|
|
1053
|
+
setDiffCommentMode(true)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const moveDiffCommentAnchor = (delta: number) => {
|
|
1057
|
+
if (diffCommentAnchors.length === 0) return
|
|
1058
|
+
const currentAnchor = selectedDiffCommentAnchor ?? diffCommentAnchors[0]
|
|
1059
|
+
const currentRowIndex = Math.max(0, currentAnchor ? diffCommentRows.indexOf(currentAnchor.renderLine) : 0)
|
|
1060
|
+
const nextRow = diffCommentRows[Math.max(0, Math.min(diffCommentRows.length - 1, currentRowIndex + delta))]
|
|
1061
|
+
if (nextRow === undefined) return
|
|
1062
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.renderLine === nextRow && anchor.side === currentAnchor?.side)
|
|
1063
|
+
?? diffCommentAnchors.find((anchor) => anchor.renderLine === nextRow)
|
|
1064
|
+
if (!nextAnchor) return
|
|
1065
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const selectDiffCommentSide = (side: DiffCommentSide) => {
|
|
1069
|
+
if (!selectedDiffCommentAnchor) return
|
|
1070
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.renderLine === selectedDiffCommentAnchor.renderLine && anchor.side === side)
|
|
1071
|
+
if (!nextAnchor) return
|
|
1072
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const selectDiffCommentLine = (renderLine: number, side: DiffCommentSide | null) => {
|
|
1076
|
+
const lineAnchors = diffCommentAnchors.filter((anchor) => anchor.renderLine === renderLine)
|
|
1077
|
+
const nextAnchor = (side ? lineAnchors.find((anchor) => anchor.side === side) : undefined) ?? lineAnchors[0]
|
|
1078
|
+
if (!nextAnchor) return
|
|
1079
|
+
suppressNextDiffCommentScrollRef.current = true
|
|
1080
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1081
|
+
setDiffFileIndex(nextAnchor.fileIndex)
|
|
1082
|
+
setDiffCommentMode(true)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const editComment = (transform: (state: CommentEditorValue) => CommentEditorValue) => {
|
|
1086
|
+
setCommentModal((current) => {
|
|
1087
|
+
const next = transform({ body: current.body, cursor: current.cursor })
|
|
1088
|
+
if (next.body === current.body && next.cursor === current.cursor && current.error === null) return current
|
|
1089
|
+
return { ...current, body: next.body, cursor: next.cursor, error: null }
|
|
1090
|
+
})
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const openDiffCommentModal = () => {
|
|
1094
|
+
if (!selectedDiffCommentAnchor || !selectedPullRequest) return
|
|
1095
|
+
setCommentModal(initialCommentModalState)
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const openDiffCommentThreadModal = () => {
|
|
1099
|
+
if (!selectedDiffCommentAnchor || selectedDiffCommentThread.length === 0) return
|
|
1100
|
+
setCommentThreadModal({ scrollOffset: 0 })
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const submitDiffComment = () => {
|
|
1104
|
+
if (!selectedPullRequest || !selectedDiffCommentAnchor) return
|
|
1105
|
+
const body = commentModal.body.trim()
|
|
1106
|
+
if (body.length === 0) {
|
|
1107
|
+
setCommentModal((current) => ({ ...current, error: "Write a comment before saving." }))
|
|
1108
|
+
return
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const threadKey = selectedDiffCommentThreadKey
|
|
1112
|
+
const target = selectedDiffCommentAnchor
|
|
1113
|
+
const optimisticComment = {
|
|
1114
|
+
id: `local:${Date.now()}`,
|
|
1115
|
+
path: target.path,
|
|
1116
|
+
line: target.line,
|
|
1117
|
+
side: target.side,
|
|
1118
|
+
author: username ?? "you",
|
|
1119
|
+
body,
|
|
1120
|
+
createdAt: new Date(),
|
|
1121
|
+
url: null,
|
|
1122
|
+
} satisfies PullRequestReviewComment
|
|
1123
|
+
const input = {
|
|
1124
|
+
repository: selectedPullRequest.repository,
|
|
1125
|
+
number: selectedPullRequest.number,
|
|
1126
|
+
commitId: selectedPullRequest.headRefOid,
|
|
1127
|
+
path: target.path,
|
|
1128
|
+
line: target.line,
|
|
1129
|
+
side: target.side,
|
|
1130
|
+
body,
|
|
1131
|
+
} satisfies CreatePullRequestCommentInput
|
|
1132
|
+
|
|
1133
|
+
if (threadKey) {
|
|
1134
|
+
setDiffCommentThreads((current) => ({
|
|
1135
|
+
...current,
|
|
1136
|
+
[threadKey]: [...(current[threadKey] ?? []), optimisticComment],
|
|
1137
|
+
}))
|
|
1138
|
+
}
|
|
1139
|
+
closeActiveModal()
|
|
1140
|
+
flashNotice(`Commenting on ${target.path}:${target.line}`)
|
|
1141
|
+
void createPullRequestComment(input).then((comment) => {
|
|
1142
|
+
if (threadKey) {
|
|
1143
|
+
setDiffCommentThreads((current) => ({
|
|
1144
|
+
...current,
|
|
1145
|
+
[threadKey]: (current[threadKey] ?? []).map((existing) => existing.id === optimisticComment.id ? comment : existing),
|
|
1146
|
+
}))
|
|
1147
|
+
}
|
|
1148
|
+
flashNotice(`Commented on ${target.path}:${target.line}`)
|
|
1149
|
+
}).catch((error) => {
|
|
1150
|
+
if (threadKey) {
|
|
1151
|
+
setDiffCommentThreads((current) => {
|
|
1152
|
+
const next = { ...current }
|
|
1153
|
+
const comments = (next[threadKey] ?? []).filter((comment) => comment.id !== optimisticComment.id)
|
|
1154
|
+
if (comments.length > 0) next[threadKey] = comments
|
|
1155
|
+
else delete next[threadKey]
|
|
1156
|
+
return next
|
|
1157
|
+
})
|
|
1158
|
+
}
|
|
1159
|
+
flashNotice(errorMessage(error))
|
|
1160
|
+
})
|
|
599
1161
|
}
|
|
600
1162
|
|
|
601
1163
|
const openSelectedPullRequestInBrowser = (pullRequest: PullRequestItem) => {
|
|
@@ -604,13 +1166,34 @@ export const App = () => {
|
|
|
604
1166
|
.catch((error) => flashNotice(errorMessage(error)))
|
|
605
1167
|
}
|
|
606
1168
|
|
|
1169
|
+
const copySelectedPullRequestMetadata = () => {
|
|
1170
|
+
if (!selectedPullRequest) return
|
|
1171
|
+
void copyPullRequestMetadata(selectedPullRequest)
|
|
1172
|
+
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1173
|
+
.catch((error) => flashNotice(errorMessage(error)))
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const toggleSelectedPullRequestDraftStatus = () => {
|
|
1177
|
+
if (!selectedPullRequest) return
|
|
1178
|
+
const previousPullRequest = selectedPullRequest
|
|
1179
|
+
const nextReviewStatus = selectedPullRequest.reviewStatus === "draft" ? "review" : "draft"
|
|
1180
|
+
updatePullRequest(selectedPullRequest.url, (pullRequest) => ({
|
|
1181
|
+
...pullRequest,
|
|
1182
|
+
reviewStatus: nextReviewStatus,
|
|
1183
|
+
}))
|
|
1184
|
+
void toggleDraftStatus({ repository: selectedPullRequest.repository, number: selectedPullRequest.number, isDraft: selectedPullRequest.reviewStatus === "draft" })
|
|
1185
|
+
.then(() => {
|
|
1186
|
+
flashNotice(selectedPullRequest.reviewStatus === "draft" ? `Marked #${selectedPullRequest.number} ready` : `Marked #${selectedPullRequest.number} draft`)
|
|
1187
|
+
})
|
|
1188
|
+
.catch((error) => {
|
|
1189
|
+
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
1190
|
+
flashNotice(errorMessage(error))
|
|
1191
|
+
})
|
|
1192
|
+
}
|
|
1193
|
+
|
|
607
1194
|
const openCloseModal = () => {
|
|
608
1195
|
if (!selectedPullRequest || selectedPullRequest.state !== "open") return
|
|
609
|
-
setLabelModal(initialLabelModalState)
|
|
610
|
-
setMergeModal(initialMergeModalState)
|
|
611
|
-
setThemeModal(initialThemeModalState)
|
|
612
1196
|
setCloseModal({
|
|
613
|
-
open: true,
|
|
614
1197
|
repository: selectedPullRequest.repository,
|
|
615
1198
|
number: selectedPullRequest.number,
|
|
616
1199
|
title: selectedPullRequest.title,
|
|
@@ -639,7 +1222,7 @@ export const App = () => {
|
|
|
639
1222
|
},
|
|
640
1223
|
}))
|
|
641
1224
|
}
|
|
642
|
-
|
|
1225
|
+
closeActiveModal()
|
|
643
1226
|
refreshPullRequests(`Closed #${number}`)
|
|
644
1227
|
})
|
|
645
1228
|
.catch((error) => {
|
|
@@ -649,11 +1232,7 @@ export const App = () => {
|
|
|
649
1232
|
}
|
|
650
1233
|
|
|
651
1234
|
const openThemeModal = () => {
|
|
652
|
-
setLabelModal(initialLabelModalState)
|
|
653
|
-
setCloseModal(initialCloseModalState)
|
|
654
|
-
setMergeModal(initialMergeModalState)
|
|
655
1235
|
setThemeModal({
|
|
656
|
-
open: true,
|
|
657
1236
|
query: "",
|
|
658
1237
|
filterMode: false,
|
|
659
1238
|
initialThemeId: themeId,
|
|
@@ -668,7 +1247,7 @@ export const App = () => {
|
|
|
668
1247
|
void Effect.runPromise(saveStoredThemeId(selectedTheme.id)).catch((error) => flashNotice(errorMessage(error)))
|
|
669
1248
|
flashNotice(`Theme: ${selectedTheme.name}`)
|
|
670
1249
|
}
|
|
671
|
-
|
|
1250
|
+
closeActiveModal()
|
|
672
1251
|
}
|
|
673
1252
|
|
|
674
1253
|
const previewTheme = (id: ThemeId) => {
|
|
@@ -711,14 +1290,10 @@ export const App = () => {
|
|
|
711
1290
|
|
|
712
1291
|
const openLabelModal = () => {
|
|
713
1292
|
if (!selectedPullRequest) return
|
|
714
|
-
setCloseModal(initialCloseModalState)
|
|
715
|
-
setMergeModal(initialMergeModalState)
|
|
716
|
-
setThemeModal(initialThemeModalState)
|
|
717
1293
|
const repository = selectedPullRequest.repository
|
|
718
1294
|
const cachedLabels = labelCache[repository]
|
|
719
1295
|
if (cachedLabels) {
|
|
720
1296
|
setLabelModal({
|
|
721
|
-
open: true,
|
|
722
1297
|
repository,
|
|
723
1298
|
query: "",
|
|
724
1299
|
selectedIndex: 0,
|
|
@@ -728,7 +1303,7 @@ export const App = () => {
|
|
|
728
1303
|
return
|
|
729
1304
|
}
|
|
730
1305
|
|
|
731
|
-
setLabelModal(
|
|
1306
|
+
setLabelModal({ repository, query: "", selectedIndex: 0, availableLabels: [], loading: true })
|
|
732
1307
|
void loadRepoLabels(repository)
|
|
733
1308
|
.then((labels) => {
|
|
734
1309
|
setLabelCache((current) => ({ ...current, [repository]: labels }))
|
|
@@ -736,20 +1311,16 @@ export const App = () => {
|
|
|
736
1311
|
})
|
|
737
1312
|
.catch((error) => {
|
|
738
1313
|
setLabelModal((current) => current.repository === repository ? { ...current, loading: false } : current)
|
|
739
|
-
flashNotice(
|
|
1314
|
+
flashNotice(errorMessage(error))
|
|
740
1315
|
})
|
|
741
1316
|
}
|
|
742
1317
|
|
|
743
1318
|
const openMergeModal = () => {
|
|
744
1319
|
if (!selectedPullRequest) return
|
|
745
|
-
setCloseModal(initialCloseModalState)
|
|
746
|
-
setThemeModal(initialThemeModalState)
|
|
747
1320
|
const repository = selectedPullRequest.repository
|
|
748
1321
|
const number = selectedPullRequest.number
|
|
749
1322
|
const seededInfo = mergeInfoFromPullRequest(selectedPullRequest)
|
|
750
|
-
setLabelModal(initialLabelModalState)
|
|
751
1323
|
setMergeModal({
|
|
752
|
-
open: true,
|
|
753
1324
|
repository,
|
|
754
1325
|
number,
|
|
755
1326
|
selectedIndex: 0,
|
|
@@ -803,7 +1374,7 @@ export const App = () => {
|
|
|
803
1374
|
},
|
|
804
1375
|
}))
|
|
805
1376
|
}
|
|
806
|
-
|
|
1377
|
+
closeActiveModal()
|
|
807
1378
|
if (option.refreshOnSuccess) {
|
|
808
1379
|
refreshPullRequests(`${option.pastTense} #${number}`)
|
|
809
1380
|
} else {
|
|
@@ -837,7 +1408,7 @@ export const App = () => {
|
|
|
837
1408
|
.then(() => flashNotice(`Removed ${label.name} from #${selectedPullRequest.number}`))
|
|
838
1409
|
.catch((error) => {
|
|
839
1410
|
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
840
|
-
flashNotice(
|
|
1411
|
+
flashNotice(errorMessage(error))
|
|
841
1412
|
})
|
|
842
1413
|
} else {
|
|
843
1414
|
updatePullRequest(selectedPullRequest.url, (pr) => ({
|
|
@@ -848,34 +1419,26 @@ export const App = () => {
|
|
|
848
1419
|
.then(() => flashNotice(`Added ${label.name} to #${selectedPullRequest.number}`))
|
|
849
1420
|
.catch((error) => {
|
|
850
1421
|
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
851
|
-
flashNotice(
|
|
1422
|
+
flashNotice(errorMessage(error))
|
|
852
1423
|
})
|
|
853
1424
|
}
|
|
854
1425
|
}
|
|
855
1426
|
|
|
856
1427
|
useKeyboard((key) => {
|
|
857
|
-
if ((key.name === "q" && !(
|
|
858
|
-
if (
|
|
1428
|
+
if ((key.name === "q" && !commentModalActive && !(themeModalActive && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
|
|
1429
|
+
if (themeModalActive) {
|
|
859
1430
|
closeThemeModal(false)
|
|
860
1431
|
return
|
|
861
1432
|
}
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
return
|
|
865
|
-
}
|
|
866
|
-
if (mergeModal.open) {
|
|
867
|
-
setMergeModal(initialMergeModalState)
|
|
868
|
-
return
|
|
869
|
-
}
|
|
870
|
-
if (labelModal.open) {
|
|
871
|
-
setLabelModal(initialLabelModalState)
|
|
1433
|
+
if (activeModal._tag !== "None") {
|
|
1434
|
+
closeActiveModal()
|
|
872
1435
|
return
|
|
873
1436
|
}
|
|
874
1437
|
renderer.destroy()
|
|
875
1438
|
return
|
|
876
1439
|
}
|
|
877
1440
|
|
|
878
|
-
if (
|
|
1441
|
+
if (themeModalActive) {
|
|
879
1442
|
if (key.name === "escape") {
|
|
880
1443
|
if (themeModal.filterMode) {
|
|
881
1444
|
updateThemeQuery("", { filterMode: false })
|
|
@@ -916,9 +1479,137 @@ export const App = () => {
|
|
|
916
1479
|
return
|
|
917
1480
|
}
|
|
918
1481
|
|
|
919
|
-
if (
|
|
1482
|
+
if (commentModalActive) {
|
|
1483
|
+
if (key.name === "escape") {
|
|
1484
|
+
closeActiveModal()
|
|
1485
|
+
return
|
|
1486
|
+
}
|
|
1487
|
+
if (key.ctrl && key.name === "s") {
|
|
1488
|
+
submitDiffComment()
|
|
1489
|
+
return
|
|
1490
|
+
}
|
|
1491
|
+
if (key.ctrl && key.name === "a") {
|
|
1492
|
+
editComment(moveLineStart)
|
|
1493
|
+
return
|
|
1494
|
+
}
|
|
1495
|
+
if (key.ctrl && key.name === "e") {
|
|
1496
|
+
editComment(moveLineEnd)
|
|
1497
|
+
return
|
|
1498
|
+
}
|
|
1499
|
+
if (key.ctrl && key.name === "b") {
|
|
1500
|
+
editComment(editorMoveLeft)
|
|
1501
|
+
return
|
|
1502
|
+
}
|
|
1503
|
+
if (key.ctrl && key.name === "f") {
|
|
1504
|
+
editComment(editorMoveRight)
|
|
1505
|
+
return
|
|
1506
|
+
}
|
|
1507
|
+
if (key.ctrl && key.name === "w") {
|
|
1508
|
+
editComment(deleteWordBackward)
|
|
1509
|
+
return
|
|
1510
|
+
}
|
|
1511
|
+
if (key.ctrl && key.name === "u") {
|
|
1512
|
+
editComment(deleteToLineStart)
|
|
1513
|
+
return
|
|
1514
|
+
}
|
|
1515
|
+
if (key.ctrl && key.name === "k") {
|
|
1516
|
+
editComment(deleteToLineEnd)
|
|
1517
|
+
return
|
|
1518
|
+
}
|
|
1519
|
+
if (key.ctrl && key.name === "d") {
|
|
1520
|
+
editComment(editorDeleteForward)
|
|
1521
|
+
return
|
|
1522
|
+
}
|
|
1523
|
+
if ((key.meta || key.option) && (key.name === "b" || key.name === "left")) {
|
|
1524
|
+
editComment(moveWordBackward)
|
|
1525
|
+
return
|
|
1526
|
+
}
|
|
1527
|
+
if ((key.meta || key.option) && (key.name === "f" || key.name === "right")) {
|
|
1528
|
+
editComment(moveWordForward)
|
|
1529
|
+
return
|
|
1530
|
+
}
|
|
1531
|
+
if ((key.meta || key.option) && (key.name === "backspace" || key.name === "delete")) {
|
|
1532
|
+
editComment(key.name === "delete" ? deleteWordForward : deleteWordBackward)
|
|
1533
|
+
return
|
|
1534
|
+
}
|
|
1535
|
+
if (key.name === "backspace") {
|
|
1536
|
+
editComment(editorBackspace)
|
|
1537
|
+
return
|
|
1538
|
+
}
|
|
1539
|
+
if (key.name === "delete") {
|
|
1540
|
+
editComment(editorDeleteForward)
|
|
1541
|
+
return
|
|
1542
|
+
}
|
|
1543
|
+
if (key.name === "left") {
|
|
1544
|
+
editComment(editorMoveLeft)
|
|
1545
|
+
return
|
|
1546
|
+
}
|
|
1547
|
+
if (key.name === "right") {
|
|
1548
|
+
editComment(editorMoveRight)
|
|
1549
|
+
return
|
|
1550
|
+
}
|
|
1551
|
+
if (key.name === "up") {
|
|
1552
|
+
editComment((state) => moveVertically(state, -1))
|
|
1553
|
+
return
|
|
1554
|
+
}
|
|
1555
|
+
if (key.name === "down") {
|
|
1556
|
+
editComment((state) => moveVertically(state, 1))
|
|
1557
|
+
return
|
|
1558
|
+
}
|
|
1559
|
+
if (key.name === "home") {
|
|
1560
|
+
editComment(moveLineStart)
|
|
1561
|
+
return
|
|
1562
|
+
}
|
|
1563
|
+
if (key.name === "end") {
|
|
1564
|
+
editComment(moveLineEnd)
|
|
1565
|
+
return
|
|
1566
|
+
}
|
|
1567
|
+
if ((key.name === "return" || key.name === "enter") && key.shift) {
|
|
1568
|
+
editComment((state) => insertText(state, "\n"))
|
|
1569
|
+
return
|
|
1570
|
+
}
|
|
1571
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1572
|
+
submitDiffComment()
|
|
1573
|
+
return
|
|
1574
|
+
}
|
|
1575
|
+
if (!key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
1576
|
+
editComment((state) => insertText(state, key.sequence))
|
|
1577
|
+
return
|
|
1578
|
+
}
|
|
1579
|
+
return
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (commentThreadModalActive) {
|
|
1583
|
+
if (key.name === "escape") {
|
|
1584
|
+
closeActiveModal()
|
|
1585
|
+
return
|
|
1586
|
+
}
|
|
1587
|
+
if (key.name === "return" || key.name === "enter" || key.name === "a" || key.name === "c") {
|
|
1588
|
+
openDiffCommentModal()
|
|
1589
|
+
return
|
|
1590
|
+
}
|
|
1591
|
+
if (key.name === "up" || key.name === "k") {
|
|
1592
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - 1) }))
|
|
1593
|
+
return
|
|
1594
|
+
}
|
|
1595
|
+
if (key.name === "down" || key.name === "j") {
|
|
1596
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: current.scrollOffset + 1 }))
|
|
1597
|
+
return
|
|
1598
|
+
}
|
|
1599
|
+
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
1600
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - halfPage) }))
|
|
1601
|
+
return
|
|
1602
|
+
}
|
|
1603
|
+
if (key.name === "pagedown" || key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
1604
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: current.scrollOffset + halfPage }))
|
|
1605
|
+
return
|
|
1606
|
+
}
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
if (closeModalActive) {
|
|
920
1611
|
if (key.name === "escape") {
|
|
921
|
-
|
|
1612
|
+
closeActiveModal()
|
|
922
1613
|
return
|
|
923
1614
|
}
|
|
924
1615
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -928,10 +1619,10 @@ export const App = () => {
|
|
|
928
1619
|
return
|
|
929
1620
|
}
|
|
930
1621
|
|
|
931
|
-
if (
|
|
1622
|
+
if (mergeModalActive) {
|
|
932
1623
|
const options = availableMergeActions(mergeModal.info)
|
|
933
1624
|
if (key.name === "escape") {
|
|
934
|
-
|
|
1625
|
+
closeActiveModal()
|
|
935
1626
|
return
|
|
936
1627
|
}
|
|
937
1628
|
if ((key.name === "return" || key.name === "enter") && options.length > 0) {
|
|
@@ -956,9 +1647,9 @@ export const App = () => {
|
|
|
956
1647
|
}
|
|
957
1648
|
|
|
958
1649
|
// Label modal takes priority over everything else
|
|
959
|
-
if (
|
|
1650
|
+
if (labelModalActive) {
|
|
960
1651
|
if (key.name === "escape") {
|
|
961
|
-
|
|
1652
|
+
closeActiveModal()
|
|
962
1653
|
return
|
|
963
1654
|
}
|
|
964
1655
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -1006,66 +1697,107 @@ export const App = () => {
|
|
|
1006
1697
|
}
|
|
1007
1698
|
|
|
1008
1699
|
if (diffFullView) {
|
|
1700
|
+
if (diffCommentMode) {
|
|
1701
|
+
if (key.name === "escape") {
|
|
1702
|
+
setDiffCommentMode(false)
|
|
1703
|
+
return
|
|
1704
|
+
}
|
|
1705
|
+
if (key.name === "c") {
|
|
1706
|
+
setDiffCommentMode(false)
|
|
1707
|
+
return
|
|
1708
|
+
}
|
|
1709
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1710
|
+
if (selectedDiffCommentThread.length > 0) openDiffCommentThreadModal()
|
|
1711
|
+
else openDiffCommentModal()
|
|
1712
|
+
return
|
|
1713
|
+
}
|
|
1714
|
+
if (key.name === "a") {
|
|
1715
|
+
openDiffCommentModal()
|
|
1716
|
+
return
|
|
1717
|
+
}
|
|
1718
|
+
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
1719
|
+
moveDiffCommentAnchor(-halfPage)
|
|
1720
|
+
return
|
|
1721
|
+
}
|
|
1722
|
+
if (key.name === "pagedown" || key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
1723
|
+
moveDiffCommentAnchor(halfPage)
|
|
1724
|
+
return
|
|
1725
|
+
}
|
|
1726
|
+
if ((key.shift || key.option || key.meta) && (key.name === "up" || key.name === "k") || key.name === "K") {
|
|
1727
|
+
moveDiffCommentAnchor(-8)
|
|
1728
|
+
return
|
|
1729
|
+
}
|
|
1730
|
+
if ((key.shift || key.option || key.meta) && (key.name === "down" || key.name === "j") || key.name === "J") {
|
|
1731
|
+
moveDiffCommentAnchor(8)
|
|
1732
|
+
return
|
|
1733
|
+
}
|
|
1734
|
+
if (key.name === "up" || key.name === "k") {
|
|
1735
|
+
moveDiffCommentAnchor(-1)
|
|
1736
|
+
return
|
|
1737
|
+
}
|
|
1738
|
+
if (key.name === "down" || key.name === "j") {
|
|
1739
|
+
moveDiffCommentAnchor(1)
|
|
1740
|
+
return
|
|
1741
|
+
}
|
|
1742
|
+
if (key.name === "left" || key.name === "h") {
|
|
1743
|
+
selectDiffCommentSide("LEFT")
|
|
1744
|
+
return
|
|
1745
|
+
}
|
|
1746
|
+
if (key.name === "right" || key.name === "l") {
|
|
1747
|
+
selectDiffCommentSide("RIGHT")
|
|
1748
|
+
return
|
|
1749
|
+
}
|
|
1750
|
+
if (key.name === "]" && selectedDiffState?._tag === "Ready") {
|
|
1751
|
+
jumpDiffFile(1)
|
|
1752
|
+
return
|
|
1753
|
+
}
|
|
1754
|
+
if (key.name === "[" && selectedDiffState?._tag === "Ready") {
|
|
1755
|
+
jumpDiffFile(-1)
|
|
1756
|
+
return
|
|
1757
|
+
}
|
|
1758
|
+
return
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1009
1761
|
if (key.name === "escape" || key.name === "return" || key.name === "enter") {
|
|
1010
1762
|
setDiffFullView(false)
|
|
1763
|
+
setDiffCommentMode(false)
|
|
1764
|
+
return
|
|
1765
|
+
}
|
|
1766
|
+
if (key.name === "c" && selectedDiffState?._tag === "Ready") {
|
|
1767
|
+
enterDiffCommentMode()
|
|
1011
1768
|
return
|
|
1012
1769
|
}
|
|
1013
1770
|
if (key.name === "home") {
|
|
1014
|
-
|
|
1771
|
+
scrollDiffTo(0)
|
|
1015
1772
|
return
|
|
1016
1773
|
}
|
|
1017
1774
|
if (key.name === "end") {
|
|
1018
|
-
|
|
1775
|
+
scrollDiffTo(Number.MAX_SAFE_INTEGER)
|
|
1019
1776
|
return
|
|
1020
1777
|
}
|
|
1021
1778
|
if (key.name === "pageup") {
|
|
1022
|
-
|
|
1779
|
+
scrollDiffBy(-halfPage)
|
|
1023
1780
|
return
|
|
1024
1781
|
}
|
|
1025
1782
|
if (key.name === "pagedown") {
|
|
1026
|
-
|
|
1027
|
-
return
|
|
1028
|
-
}
|
|
1029
|
-
if (isShiftG(key)) {
|
|
1030
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER })
|
|
1031
|
-
setPendingG(false)
|
|
1032
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1033
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1034
|
-
pendingGTimeoutRef.current = null
|
|
1035
|
-
}
|
|
1036
|
-
return
|
|
1037
|
-
}
|
|
1038
|
-
if (key.name === "g") {
|
|
1039
|
-
if (pendingG) {
|
|
1040
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1041
|
-
setPendingG(false)
|
|
1042
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1043
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1044
|
-
pendingGTimeoutRef.current = null
|
|
1045
|
-
}
|
|
1046
|
-
} else {
|
|
1047
|
-
setPendingG(true)
|
|
1048
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1049
|
-
setPendingG(false)
|
|
1050
|
-
pendingGTimeoutRef.current = null
|
|
1051
|
-
}, 500)
|
|
1052
|
-
}
|
|
1783
|
+
scrollDiffBy(halfPage)
|
|
1053
1784
|
return
|
|
1054
1785
|
}
|
|
1786
|
+
if (handleVimGoto(key, () => scrollDiffTo(0), () => scrollDiffTo(Number.MAX_SAFE_INTEGER))) return
|
|
1055
1787
|
if (key.name === "up" || key.name === "k") {
|
|
1056
|
-
|
|
1788
|
+
scrollDiffBy(-1)
|
|
1057
1789
|
return
|
|
1058
1790
|
}
|
|
1059
1791
|
if (key.name === "down" || key.name === "j") {
|
|
1060
|
-
|
|
1792
|
+
scrollDiffBy(1)
|
|
1061
1793
|
return
|
|
1062
1794
|
}
|
|
1063
1795
|
if (key.ctrl && key.name === "u") {
|
|
1064
|
-
|
|
1796
|
+
scrollDiffBy(-halfPage)
|
|
1065
1797
|
return
|
|
1066
1798
|
}
|
|
1067
1799
|
if (key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
1068
|
-
|
|
1800
|
+
scrollDiffBy(halfPage)
|
|
1069
1801
|
return
|
|
1070
1802
|
}
|
|
1071
1803
|
if (key.name === "v") {
|
|
@@ -1077,18 +1809,16 @@ export const App = () => {
|
|
|
1077
1809
|
return
|
|
1078
1810
|
}
|
|
1079
1811
|
if (key.name === "r" && selectedPullRequest) {
|
|
1080
|
-
loadPullRequestDiff(selectedPullRequest, true)
|
|
1812
|
+
loadPullRequestDiff(selectedPullRequest, { force: true, includeComments: true })
|
|
1081
1813
|
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
1082
1814
|
return
|
|
1083
1815
|
}
|
|
1084
|
-
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?.
|
|
1085
|
-
|
|
1086
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1816
|
+
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?._tag === "Ready") {
|
|
1817
|
+
jumpDiffFile(1)
|
|
1087
1818
|
return
|
|
1088
1819
|
}
|
|
1089
|
-
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?.
|
|
1090
|
-
|
|
1091
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1820
|
+
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?._tag === "Ready") {
|
|
1821
|
+
jumpDiffFile(-1)
|
|
1092
1822
|
return
|
|
1093
1823
|
}
|
|
1094
1824
|
if (key.name === "o" && selectedPullRequest) {
|
|
@@ -1100,24 +1830,48 @@ export const App = () => {
|
|
|
1100
1830
|
|
|
1101
1831
|
// Fullscreen detail mode handles its own navigation keys.
|
|
1102
1832
|
if (detailFullView) {
|
|
1833
|
+
const plainKey = !key.ctrl && !key.meta && !key.option
|
|
1103
1834
|
if (key.name === "escape" || (key.name === "return" || key.name === "enter")) {
|
|
1104
1835
|
setDetailFullView(false)
|
|
1105
1836
|
setDetailScrollOffset(0)
|
|
1106
1837
|
return
|
|
1107
1838
|
}
|
|
1839
|
+
if (isThemeKey(key)) {
|
|
1840
|
+
openThemeModal()
|
|
1841
|
+
return
|
|
1842
|
+
}
|
|
1843
|
+
if (plainKey && key.name === "d" && selectedPullRequest) {
|
|
1844
|
+
openDiffView()
|
|
1845
|
+
return
|
|
1846
|
+
}
|
|
1847
|
+
if (plainKey && key.name === "x" && selectedPullRequest?.state === "open") {
|
|
1848
|
+
openCloseModal()
|
|
1849
|
+
return
|
|
1850
|
+
}
|
|
1851
|
+
if (plainKey && key.name === "l" && selectedPullRequest) {
|
|
1852
|
+
openLabelModal()
|
|
1853
|
+
return
|
|
1854
|
+
}
|
|
1855
|
+
if (plainKey && (key.name === "m" || key.name === "M") && selectedPullRequest) {
|
|
1856
|
+
openMergeModal()
|
|
1857
|
+
return
|
|
1858
|
+
}
|
|
1859
|
+
if (plainKey && (key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
1860
|
+
toggleSelectedPullRequestDraftStatus()
|
|
1861
|
+
return
|
|
1862
|
+
}
|
|
1863
|
+
if (plainKey && key.name === "r") {
|
|
1864
|
+
refreshPullRequests("Refreshed")
|
|
1865
|
+
return
|
|
1866
|
+
}
|
|
1108
1867
|
if (key.name === "home") {
|
|
1109
1868
|
detailScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1110
1869
|
setDetailScrollOffset(0)
|
|
1111
1870
|
return
|
|
1112
1871
|
}
|
|
1113
|
-
if (key.name === "end"
|
|
1872
|
+
if (key.name === "end") {
|
|
1114
1873
|
detailScrollRef.current?.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER })
|
|
1115
1874
|
setDetailScrollOffset(Number.MAX_SAFE_INTEGER)
|
|
1116
|
-
setPendingG(false)
|
|
1117
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1118
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1119
|
-
pendingGTimeoutRef.current = null
|
|
1120
|
-
}
|
|
1121
1875
|
return
|
|
1122
1876
|
}
|
|
1123
1877
|
if (key.name === "pageup") {
|
|
@@ -1130,24 +1884,10 @@ export const App = () => {
|
|
|
1130
1884
|
setDetailScrollOffset((current) => current + halfPage)
|
|
1131
1885
|
return
|
|
1132
1886
|
}
|
|
1133
|
-
if (key
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
setPendingG(false)
|
|
1138
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1139
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1140
|
-
pendingGTimeoutRef.current = null
|
|
1141
|
-
}
|
|
1142
|
-
} else {
|
|
1143
|
-
setPendingG(true)
|
|
1144
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1145
|
-
setPendingG(false)
|
|
1146
|
-
pendingGTimeoutRef.current = null
|
|
1147
|
-
}, 500)
|
|
1148
|
-
}
|
|
1149
|
-
return
|
|
1150
|
-
}
|
|
1887
|
+
if (handleVimGoto(key,
|
|
1888
|
+
() => { detailScrollRef.current?.scrollTo({ x: 0, y: 0 }); setDetailScrollOffset(0) },
|
|
1889
|
+
() => { detailScrollRef.current?.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER }); setDetailScrollOffset(Number.MAX_SAFE_INTEGER) },
|
|
1890
|
+
)) return
|
|
1151
1891
|
if (key.name === "up" || key.name === "k") {
|
|
1152
1892
|
detailScrollRef.current?.scrollBy({ x: 0, y: -1 })
|
|
1153
1893
|
setDetailScrollOffset((current) => Math.max(0, current - 1))
|
|
@@ -1168,14 +1908,12 @@ export const App = () => {
|
|
|
1168
1908
|
setDetailScrollOffset((current) => current + halfPage)
|
|
1169
1909
|
return
|
|
1170
1910
|
}
|
|
1171
|
-
if (key.name === "o" && selectedPullRequest) {
|
|
1911
|
+
if (plainKey && key.name === "o" && selectedPullRequest) {
|
|
1172
1912
|
openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
1173
1913
|
return
|
|
1174
1914
|
}
|
|
1175
|
-
if (key.name === "y" && selectedPullRequest) {
|
|
1176
|
-
|
|
1177
|
-
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1178
|
-
.catch((error) => flashNotice(error instanceof Error ? error.message : String(error)))
|
|
1915
|
+
if (plainKey && key.name === "y" && selectedPullRequest) {
|
|
1916
|
+
copySelectedPullRequestMetadata()
|
|
1179
1917
|
return
|
|
1180
1918
|
}
|
|
1181
1919
|
return
|
|
@@ -1210,6 +1948,11 @@ export const App = () => {
|
|
|
1210
1948
|
}
|
|
1211
1949
|
}
|
|
1212
1950
|
|
|
1951
|
+
if (key.name === "tab") {
|
|
1952
|
+
switchQueueMode(key.shift ? -1 : 1)
|
|
1953
|
+
return
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1213
1956
|
if (isThemeKey(key)) {
|
|
1214
1957
|
openThemeModal()
|
|
1215
1958
|
return
|
|
@@ -1286,37 +2029,16 @@ export const App = () => {
|
|
|
1286
2029
|
})
|
|
1287
2030
|
return
|
|
1288
2031
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
return visiblePullRequests.length - 1
|
|
1294
|
-
})
|
|
1295
|
-
return
|
|
1296
|
-
}
|
|
1297
|
-
if (key.name === "g") {
|
|
1298
|
-
if (pendingG) {
|
|
1299
|
-
setSelectedIndex(0)
|
|
1300
|
-
setPendingG(false)
|
|
1301
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1302
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1303
|
-
pendingGTimeoutRef.current = null
|
|
1304
|
-
}
|
|
1305
|
-
} else {
|
|
1306
|
-
setPendingG(true)
|
|
1307
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1308
|
-
setPendingG(false)
|
|
1309
|
-
pendingGTimeoutRef.current = null
|
|
1310
|
-
}, 500)
|
|
1311
|
-
}
|
|
1312
|
-
return
|
|
1313
|
-
}
|
|
2032
|
+
if (handleVimGoto(key,
|
|
2033
|
+
() => setSelectedIndex(0),
|
|
2034
|
+
() => setSelectedIndex(visiblePullRequests.length === 0 ? 0 : visiblePullRequests.length - 1),
|
|
2035
|
+
)) return
|
|
1314
2036
|
if ((key.name === "return" || key.name === "enter") && !detailFullView) {
|
|
1315
2037
|
setDetailFullView(true)
|
|
1316
2038
|
setDetailScrollOffset(0)
|
|
1317
2039
|
return
|
|
1318
2040
|
}
|
|
1319
|
-
if (
|
|
2041
|
+
if (key.name === "d" && selectedPullRequest) {
|
|
1320
2042
|
openDiffView()
|
|
1321
2043
|
return
|
|
1322
2044
|
}
|
|
@@ -1337,30 +2059,12 @@ export const App = () => {
|
|
|
1337
2059
|
return
|
|
1338
2060
|
}
|
|
1339
2061
|
if ((key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
1340
|
-
|
|
1341
|
-
const nextReviewStatus = selectedPullRequest.reviewStatus === "draft" ? "review" : "draft"
|
|
1342
|
-
updatePullRequest(selectedPullRequest.url, (pullRequest) => ({
|
|
1343
|
-
...pullRequest,
|
|
1344
|
-
reviewStatus: nextReviewStatus,
|
|
1345
|
-
}))
|
|
1346
|
-
void toggleDraftStatus({ repository: selectedPullRequest.repository, number: selectedPullRequest.number, isDraft: selectedPullRequest.reviewStatus === "draft" })
|
|
1347
|
-
.then(() => {
|
|
1348
|
-
flashNotice(selectedPullRequest.reviewStatus === "draft" ? `Marked #${selectedPullRequest.number} ready` : `Marked #${selectedPullRequest.number} draft`)
|
|
1349
|
-
})
|
|
1350
|
-
.catch((error) => {
|
|
1351
|
-
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
1352
|
-
flashNotice(error instanceof Error ? error.message : String(error))
|
|
1353
|
-
})
|
|
2062
|
+
toggleSelectedPullRequestDraftStatus()
|
|
1354
2063
|
return
|
|
1355
2064
|
}
|
|
1356
2065
|
if (key.name === "y" && selectedPullRequest) {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
flashNotice(`Copied #${selectedPullRequest.number} metadata`)
|
|
1360
|
-
})
|
|
1361
|
-
.catch((error) => {
|
|
1362
|
-
flashNotice(error instanceof Error ? error.message : String(error))
|
|
1363
|
-
})
|
|
2066
|
+
copySelectedPullRequestMetadata()
|
|
2067
|
+
return
|
|
1364
2068
|
}
|
|
1365
2069
|
})
|
|
1366
2070
|
|
|
@@ -1397,20 +2101,31 @@ export const App = () => {
|
|
|
1397
2101
|
const longestLabelName = labelModal.availableLabels.reduce((max, label) => Math.max(max, label.name.length), 0)
|
|
1398
2102
|
const labelModalWidth = Math.min(Math.max(42, longestLabelName + 16), 56, contentWidth - 4)
|
|
1399
2103
|
const labelModalHeight = Math.min(20, terminalHeight - 4)
|
|
1400
|
-
const labelModalLeft =
|
|
1401
|
-
const labelModalTop =
|
|
2104
|
+
const labelModalLeft = centeredOffset(contentWidth, labelModalWidth)
|
|
2105
|
+
const labelModalTop = centeredOffset(terminalHeight, labelModalHeight)
|
|
1402
2106
|
const closeModalWidth = Math.min(68, Math.max(46, contentWidth - 12))
|
|
1403
2107
|
const closeModalHeight = Math.min(12, terminalHeight - 4)
|
|
1404
|
-
const closeModalLeft =
|
|
1405
|
-
const closeModalTop =
|
|
2108
|
+
const closeModalLeft = centeredOffset(contentWidth, closeModalWidth)
|
|
2109
|
+
const closeModalTop = centeredOffset(terminalHeight, closeModalHeight)
|
|
2110
|
+
const commentModalWidth = Math.min(76, Math.max(46, contentWidth - 8))
|
|
2111
|
+
const commentModalHeight = Math.min(16, terminalHeight - 4)
|
|
2112
|
+
const commentModalLeft = centeredOffset(contentWidth, commentModalWidth)
|
|
2113
|
+
const commentModalTop = centeredOffset(terminalHeight, commentModalHeight)
|
|
2114
|
+
const commentThreadModalWidth = Math.min(86, Math.max(50, contentWidth - 8))
|
|
2115
|
+
const commentThreadModalHeight = Math.min(22, terminalHeight - 4)
|
|
2116
|
+
const commentThreadModalLeft = centeredOffset(contentWidth, commentThreadModalWidth)
|
|
2117
|
+
const commentThreadModalTop = centeredOffset(terminalHeight, commentThreadModalHeight)
|
|
2118
|
+
const commentAnchorLabel = selectedDiffCommentAnchor
|
|
2119
|
+
? `${selectedDiffCommentAnchor.path}:${selectedDiffCommentAnchor.line} ${selectedDiffCommentAnchor.side === "RIGHT" ? "right" : "left"}`
|
|
2120
|
+
: "No diff line selected"
|
|
1406
2121
|
const mergeModalWidth = Math.min(68, Math.max(46, contentWidth - 12))
|
|
1407
2122
|
const mergeModalHeight = Math.min(16, terminalHeight - 4)
|
|
1408
|
-
const mergeModalLeft =
|
|
1409
|
-
const mergeModalTop =
|
|
2123
|
+
const mergeModalLeft = centeredOffset(contentWidth, mergeModalWidth)
|
|
2124
|
+
const mergeModalTop = centeredOffset(terminalHeight, mergeModalHeight)
|
|
1410
2125
|
const themeModalWidth = Math.min(58, Math.max(38, contentWidth - 12))
|
|
1411
2126
|
const themeModalHeight = Math.min(16, terminalHeight - 4)
|
|
1412
|
-
const themeModalLeft =
|
|
1413
|
-
const themeModalTop =
|
|
2127
|
+
const themeModalLeft = centeredOffset(contentWidth, themeModalWidth)
|
|
2128
|
+
const themeModalTop = centeredOffset(terminalHeight, themeModalHeight)
|
|
1414
2129
|
|
|
1415
2130
|
return (
|
|
1416
2131
|
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
@@ -1428,13 +2143,19 @@ export const App = () => {
|
|
|
1428
2143
|
<PullRequestDiffPane
|
|
1429
2144
|
pullRequest={selectedPullRequest}
|
|
1430
2145
|
diffState={selectedDiffState}
|
|
1431
|
-
|
|
2146
|
+
stackedFiles={stackedDiffFiles}
|
|
2147
|
+
scrollTop={diffScrollTop}
|
|
1432
2148
|
view={effectiveDiffRenderView}
|
|
1433
2149
|
wrapMode={diffWrapMode}
|
|
1434
2150
|
paneWidth={contentWidth}
|
|
1435
2151
|
height={wideBodyHeight}
|
|
1436
2152
|
loadingIndicator={loadingIndicator}
|
|
1437
2153
|
scrollRef={diffScrollRef}
|
|
2154
|
+
setDiffRef={setDiffRenderableRef}
|
|
2155
|
+
commentMode={diffCommentMode}
|
|
2156
|
+
selectedCommentAnchor={selectedDiffCommentAnchor}
|
|
2157
|
+
selectedCommentThread={selectedDiffCommentThread}
|
|
2158
|
+
onSelectCommentLine={selectDiffCommentLine}
|
|
1438
2159
|
themeId={themeId}
|
|
1439
2160
|
/>
|
|
1440
2161
|
) : isWideLayout && detailFullView ? (
|
|
@@ -1442,6 +2163,7 @@ export const App = () => {
|
|
|
1442
2163
|
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: wideFullscreenDetailScrollable }}>
|
|
1443
2164
|
<DetailsPane
|
|
1444
2165
|
pullRequest={selectedPullRequest}
|
|
2166
|
+
viewerUsername={username}
|
|
1445
2167
|
contentWidth={fullscreenContentWidth}
|
|
1446
2168
|
bodyLines={fullscreenBodyLines}
|
|
1447
2169
|
paneWidth={contentWidth}
|
|
@@ -1453,17 +2175,17 @@ export const App = () => {
|
|
|
1453
2175
|
</scrollbox>
|
|
1454
2176
|
</box>
|
|
1455
2177
|
) : isWideLayout ? (
|
|
1456
|
-
<box flexGrow={1} flexDirection="row">
|
|
2178
|
+
<box key="wide-main" flexGrow={1} flexDirection="row">
|
|
1457
2179
|
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column" paddingLeft={sectionPadding} paddingRight={sectionPadding}>
|
|
1458
2180
|
<scrollbox height={wideBodyHeight} flexGrow={0}>
|
|
1459
|
-
<PullRequestList {...prListProps} contentWidth={leftContentWidth} />
|
|
2181
|
+
<PullRequestList key={`wide-${leftContentWidth}`} {...prListProps} contentWidth={leftContentWidth} />
|
|
1460
2182
|
</scrollbox>
|
|
1461
2183
|
</box>
|
|
1462
2184
|
<SeparatorColumn height={wideBodyHeight} junctionRows={detailJunctions} />
|
|
1463
2185
|
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
1464
2186
|
{selectedPullRequest ? (
|
|
1465
2187
|
<>
|
|
1466
|
-
<DetailHeader pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2188
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
1467
2189
|
<scrollbox flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
1468
2190
|
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
1469
2191
|
</scrollbox>
|
|
@@ -1478,6 +2200,7 @@ export const App = () => {
|
|
|
1478
2200
|
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: narrowFullscreenDetailScrollable }}>
|
|
1479
2201
|
<DetailsPane
|
|
1480
2202
|
pullRequest={selectedPullRequest}
|
|
2203
|
+
viewerUsername={username}
|
|
1481
2204
|
contentWidth={fullscreenContentWidth}
|
|
1482
2205
|
bodyLines={fullscreenBodyLines}
|
|
1483
2206
|
paneWidth={contentWidth}
|
|
@@ -1488,13 +2211,13 @@ export const App = () => {
|
|
|
1488
2211
|
</scrollbox>
|
|
1489
2212
|
</box>
|
|
1490
2213
|
) : (
|
|
1491
|
-
<box height={wideBodyHeight} flexDirection="column">
|
|
1492
|
-
<DetailsPane pullRequest={selectedPullRequest} contentWidth={
|
|
2214
|
+
<box key="narrow-main" height={wideBodyHeight} flexDirection="column">
|
|
2215
|
+
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
1493
2216
|
<Divider width={contentWidth} />
|
|
1494
2217
|
<box flexGrow={1} flexDirection="column">
|
|
1495
2218
|
<scrollbox flexGrow={1}>
|
|
1496
2219
|
<box paddingLeft={sectionPadding} paddingRight={sectionPadding}>
|
|
1497
|
-
<PullRequestList {...prListProps} contentWidth={
|
|
2220
|
+
<PullRequestList key={`narrow-${fullscreenContentWidth}`} {...prListProps} contentWidth={fullscreenContentWidth} />
|
|
1498
2221
|
</box>
|
|
1499
2222
|
</scrollbox>
|
|
1500
2223
|
</box>
|
|
@@ -1515,6 +2238,7 @@ export const App = () => {
|
|
|
1515
2238
|
showFilterClear={filterMode || filterQuery.length > 0}
|
|
1516
2239
|
detailFullView={detailFullView}
|
|
1517
2240
|
diffFullView={diffFullView}
|
|
2241
|
+
diffCommentMode={diffCommentMode}
|
|
1518
2242
|
hasSelection={selectedPullRequest !== null}
|
|
1519
2243
|
canCloseSelection={selectedPullRequest?.state === "open"}
|
|
1520
2244
|
hasError={pullRequestStatus === "error"}
|
|
@@ -1524,7 +2248,7 @@ export const App = () => {
|
|
|
1524
2248
|
/>
|
|
1525
2249
|
)}
|
|
1526
2250
|
</box>
|
|
1527
|
-
{
|
|
2251
|
+
{labelModalActive ? (
|
|
1528
2252
|
<LabelModal
|
|
1529
2253
|
state={labelModal}
|
|
1530
2254
|
currentLabels={selectedPullRequest?.labels ?? []}
|
|
@@ -1535,7 +2259,7 @@ export const App = () => {
|
|
|
1535
2259
|
loadingIndicator={loadingIndicator}
|
|
1536
2260
|
/>
|
|
1537
2261
|
) : null}
|
|
1538
|
-
{
|
|
2262
|
+
{closeModalActive ? (
|
|
1539
2263
|
<CloseModal
|
|
1540
2264
|
state={closeModal}
|
|
1541
2265
|
modalWidth={closeModalWidth}
|
|
@@ -1545,7 +2269,28 @@ export const App = () => {
|
|
|
1545
2269
|
loadingIndicator={loadingIndicator}
|
|
1546
2270
|
/>
|
|
1547
2271
|
) : null}
|
|
1548
|
-
{
|
|
2272
|
+
{commentModalActive ? (
|
|
2273
|
+
<CommentModal
|
|
2274
|
+
state={commentModal}
|
|
2275
|
+
anchorLabel={commentAnchorLabel}
|
|
2276
|
+
modalWidth={commentModalWidth}
|
|
2277
|
+
modalHeight={commentModalHeight}
|
|
2278
|
+
offsetLeft={commentModalLeft}
|
|
2279
|
+
offsetTop={commentModalTop}
|
|
2280
|
+
/>
|
|
2281
|
+
) : null}
|
|
2282
|
+
{commentThreadModalActive ? (
|
|
2283
|
+
<CommentThreadModal
|
|
2284
|
+
state={commentThreadModal}
|
|
2285
|
+
anchorLabel={commentAnchorLabel}
|
|
2286
|
+
comments={selectedDiffCommentThread}
|
|
2287
|
+
modalWidth={commentThreadModalWidth}
|
|
2288
|
+
modalHeight={commentThreadModalHeight}
|
|
2289
|
+
offsetLeft={commentThreadModalLeft}
|
|
2290
|
+
offsetTop={commentThreadModalTop}
|
|
2291
|
+
/>
|
|
2292
|
+
) : null}
|
|
2293
|
+
{mergeModalActive ? (
|
|
1549
2294
|
<MergeModal
|
|
1550
2295
|
state={mergeModal}
|
|
1551
2296
|
modalWidth={mergeModalWidth}
|
|
@@ -1555,7 +2300,7 @@ export const App = () => {
|
|
|
1555
2300
|
loadingIndicator={loadingIndicator}
|
|
1556
2301
|
/>
|
|
1557
2302
|
) : null}
|
|
1558
|
-
{
|
|
2303
|
+
{themeModalActive ? (
|
|
1559
2304
|
<ThemeModal
|
|
1560
2305
|
state={themeModal}
|
|
1561
2306
|
activeThemeId={themeId}
|