@kitlangton/ghui 0.1.16 → 0.1.18
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 +29 -27
- package/package.json +2 -1
- package/src/App.tsx +1006 -132
- package/src/domain.ts +47 -1
- package/src/services/CommandRunner.ts +8 -1
- package/src/services/GitHubService.ts +286 -184
- package/src/ui/DetailsPane.tsx +26 -25
- package/src/ui/FooterHints.tsx +60 -0
- package/src/ui/PullRequestDiffPane.tsx +105 -33
- package/src/ui/PullRequestList.tsx +38 -13
- package/src/ui/colors.ts +41 -0
- package/src/ui/commentEditor.ts +126 -0
- package/src/ui/diff.ts +204 -7
- package/src/ui/modals.tsx +322 -8
- package/src/ui/pullRequests.ts +2 -0
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 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, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles, type DiffCommentAnchor, type PullRequestDiffState, 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
19
|
import { FooterHints, type RetryProgress } from "./ui/FooterHints.js"
|
|
19
20
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
20
|
-
import { 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"
|
|
@@ -28,8 +29,10 @@ const initialThemeId = await Effect.runPromise(loadStoredThemeId)
|
|
|
28
29
|
type LoadStatus = "loading" | "ready" | "error"
|
|
29
30
|
|
|
30
31
|
interface PullRequestLoad {
|
|
32
|
+
readonly queueMode: PullRequestQueueMode
|
|
31
33
|
readonly data: readonly PullRequestItem[]
|
|
32
34
|
readonly fetchedAt: Date | null
|
|
35
|
+
readonly detailsFetchedAt: Date | null
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
interface DetailPlaceholderInput {
|
|
@@ -40,18 +43,67 @@ interface DetailPlaceholderInput {
|
|
|
40
43
|
readonly filterText: string
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
type DiffLineColorConfig = {
|
|
47
|
+
readonly gutter: string
|
|
48
|
+
readonly content: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type DiffSideRenderable = {
|
|
52
|
+
readonly setLineColor: (line: number, color: DiffLineColorConfig) => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type DiffRenderableRuntimeSides = {
|
|
56
|
+
readonly leftSide?: DiffSideRenderable
|
|
57
|
+
readonly rightSide?: DiffSideRenderable
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface AppliedDiffLineColor {
|
|
61
|
+
readonly anchor: StackedDiffCommentAnchor
|
|
62
|
+
readonly view: "unified" | "split"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface AppliedDiffLineColorState {
|
|
66
|
+
readonly contextKey: string | null
|
|
67
|
+
readonly entries: readonly AppliedDiffLineColor[]
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
const PR_FETCH_RETRIES = 6
|
|
44
71
|
const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
45
72
|
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
46
73
|
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
47
74
|
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
48
75
|
|
|
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
|
+
|
|
49
97
|
const retryProgressAtom = Atom.make<RetryProgress | null>(null).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*() {
|
|
104
|
+
const queueMode = yield* Atom.get(queueModeAtom)
|
|
53
105
|
yield* Atom.set(retryProgressAtom, null)
|
|
54
|
-
const data = yield* github.listOpenPullRequests().pipe(
|
|
106
|
+
const data = yield* github.listOpenPullRequests(queueMode).pipe(
|
|
55
107
|
Effect.tapError(() =>
|
|
56
108
|
Atom.update(retryProgressAtom, (current) => ({
|
|
57
109
|
attempt: Math.min((current?.attempt ?? 0) + 1, PR_FETCH_RETRIES),
|
|
@@ -63,7 +115,15 @@ const pullRequestsAtom = githubRuntime.atom(
|
|
|
63
115
|
)
|
|
64
116
|
|
|
65
117
|
yield* Atom.set(retryProgressAtom, null)
|
|
66
|
-
|
|
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)
|
|
@@ -79,14 +139,17 @@ const diffFullViewAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
|
79
139
|
const diffFileIndexAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
80
140
|
const diffRenderViewAtom = Atom.make<"unified" | "split">("split").pipe(Atom.keepAlive)
|
|
81
141
|
const diffWrapModeAtom = Atom.make<"none" | "word">("none").pipe(Atom.keepAlive)
|
|
142
|
+
const diffCommentModeAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
143
|
+
const diffCommentAnchorIndexAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
144
|
+
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
145
|
+
const diffCommentsLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
82
146
|
const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>({}).pipe(Atom.keepAlive)
|
|
83
147
|
|
|
84
|
-
const
|
|
85
|
-
const mergeModalAtom = Atom.make(initialMergeModalState).pipe(Atom.keepAlive)
|
|
148
|
+
const activeModalAtom = Atom.make<Modal>(initialModal).pipe(Atom.keepAlive)
|
|
86
149
|
const themeIdAtom = Atom.make<ThemeId>(initialThemeId).pipe(Atom.keepAlive)
|
|
87
|
-
const themeModalAtom = Atom.make(initialThemeModalState).pipe(Atom.keepAlive)
|
|
88
150
|
const labelCacheAtom = Atom.make<Record<string, readonly PullRequestLabel[]>>({}).pipe(Atom.keepAlive)
|
|
89
151
|
const pullRequestOverridesAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
152
|
+
const recentlyCompletedPullRequestsAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
90
153
|
const usernameAtom = githubRuntime.atom(
|
|
91
154
|
config.author === "@me"
|
|
92
155
|
? GitHubService.use((github) => github.getAuthenticatedUser())
|
|
@@ -96,8 +159,8 @@ const usernameAtom = githubRuntime.atom(
|
|
|
96
159
|
const listRepoLabelsAtom = githubRuntime.fn<string>()((repository) =>
|
|
97
160
|
GitHubService.use((github) => github.listRepoLabels(repository))
|
|
98
161
|
)
|
|
99
|
-
const listOpenPullRequestDetailsAtom = githubRuntime.fn<
|
|
100
|
-
GitHubService.use((github) => github.listOpenPullRequestDetails())
|
|
162
|
+
const listOpenPullRequestDetailsAtom = githubRuntime.fn<PullRequestQueueMode>()((queueMode) =>
|
|
163
|
+
GitHubService.use((github) => github.listOpenPullRequestDetails(queueMode))
|
|
101
164
|
)
|
|
102
165
|
const addPullRequestLabelAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly label: string }>()((input) =>
|
|
103
166
|
GitHubService.use((github) => github.addPullRequestLabel(input.repository, input.number, input.label))
|
|
@@ -111,17 +174,41 @@ const toggleDraftAtom = githubRuntime.fn<{ readonly repository: string; readonly
|
|
|
111
174
|
const getPullRequestDiffAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
112
175
|
GitHubService.use((github) => github.getPullRequestDiff(input.repository, input.number))
|
|
113
176
|
)
|
|
177
|
+
const listPullRequestCommentsAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
178
|
+
GitHubService.use((github) => github.listPullRequestComments(input.repository, input.number))
|
|
179
|
+
)
|
|
114
180
|
const getPullRequestMergeInfoAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
115
181
|
GitHubService.use((github) => github.getPullRequestMergeInfo(input.repository, input.number))
|
|
116
182
|
)
|
|
117
183
|
const mergePullRequestAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly action: PullRequestMergeAction }>()((input) =>
|
|
118
184
|
GitHubService.use((github) => github.mergePullRequest(input.repository, input.number, input.action))
|
|
119
185
|
)
|
|
186
|
+
const closePullRequestAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
187
|
+
GitHubService.use((github) => github.closePullRequest(input.repository, input.number))
|
|
188
|
+
)
|
|
189
|
+
const createPullRequestCommentAtom = githubRuntime.fn<CreatePullRequestCommentInput>()((input) => GitHubService.use((github) => github.createPullRequestComment(input)))
|
|
190
|
+
|
|
191
|
+
const centeredOffset = (outer: number, inner: number) => Math.floor((outer - inner) / 2)
|
|
120
192
|
|
|
121
193
|
const deleteLastWord = (value: string) => value.replace(/\s*\S+\s*$/, "")
|
|
122
194
|
|
|
123
195
|
const errorMessage = (error: unknown) => error instanceof Error ? error.message : String(error)
|
|
124
196
|
|
|
197
|
+
const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) => {
|
|
198
|
+
const normalized = query.trim().toLowerCase()
|
|
199
|
+
if (normalized.length === 0) return 0
|
|
200
|
+
const fields = [
|
|
201
|
+
pullRequest.title.toLowerCase(),
|
|
202
|
+
pullRequest.repository.toLowerCase(),
|
|
203
|
+
String(pullRequest.number),
|
|
204
|
+
]
|
|
205
|
+
const scores = fields.flatMap((field, index) => {
|
|
206
|
+
const matchIndex = field.indexOf(normalized)
|
|
207
|
+
return matchIndex >= 0 ? [index * 1000 + matchIndex] : []
|
|
208
|
+
})
|
|
209
|
+
return scores.length > 0 ? Math.min(...scores) : null
|
|
210
|
+
}
|
|
211
|
+
|
|
125
212
|
const clipboardCommands = (): readonly (readonly string[])[] => {
|
|
126
213
|
if (process.platform === "darwin") return [["pbcopy"]]
|
|
127
214
|
if (process.platform === "linux") {
|
|
@@ -202,6 +289,52 @@ const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => k
|
|
|
202
289
|
|
|
203
290
|
const isThemeKey = (key: { readonly name: string; readonly ctrl?: boolean; readonly meta?: boolean }) => !key.ctrl && !key.meta && key.name.toLowerCase() === "t"
|
|
204
291
|
|
|
292
|
+
const nextQueueMode = (mode: PullRequestQueueMode, delta: 1 | -1) => {
|
|
293
|
+
const index = pullRequestQueueModes.indexOf(mode)
|
|
294
|
+
return pullRequestQueueModes[(index + delta + pullRequestQueueModes.length) % pullRequestQueueModes.length]!
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const diffCommentThreadKey = (pullRequest: PullRequestItem, comment: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
298
|
+
`${pullRequestDiffKey(pullRequest)}:${diffCommentLocationKey(comment)}`
|
|
299
|
+
|
|
300
|
+
const groupDiffCommentThreads = (pullRequest: PullRequestItem, comments: readonly PullRequestReviewComment[]) => {
|
|
301
|
+
const threads: Record<string, PullRequestReviewComment[]> = {}
|
|
302
|
+
for (const comment of comments) {
|
|
303
|
+
const key = diffCommentThreadKey(pullRequest, comment)
|
|
304
|
+
const thread = threads[key]
|
|
305
|
+
if (thread) thread.push(comment)
|
|
306
|
+
else threads[key] = [comment]
|
|
307
|
+
}
|
|
308
|
+
return threads
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const isLocalDiffComment = (comment: PullRequestReviewComment) => comment.id.startsWith("local:")
|
|
312
|
+
|
|
313
|
+
const originalDiffLineColor = (anchor: DiffCommentAnchor): DiffLineColorConfig => {
|
|
314
|
+
if (anchor.kind === "addition") {
|
|
315
|
+
return { gutter: colors.diff.addedLineNumberBg, content: colors.diff.addedBg }
|
|
316
|
+
}
|
|
317
|
+
if (anchor.kind === "deletion") {
|
|
318
|
+
return { gutter: colors.diff.removedLineNumberBg, content: colors.diff.removedBg }
|
|
319
|
+
}
|
|
320
|
+
return { gutter: colors.diff.lineNumberBg, content: colors.diff.contextBg }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const diffSideTargets = (diff: DiffRenderable, anchor: DiffCommentAnchor, view: "unified" | "split") => {
|
|
324
|
+
const withSides = diff as unknown as DiffRenderableRuntimeSides
|
|
325
|
+
if (view === "split") {
|
|
326
|
+
const target = anchor.side === "LEFT" ? withSides.leftSide : withSides.rightSide
|
|
327
|
+
return target ? [target] : []
|
|
328
|
+
}
|
|
329
|
+
return withSides.leftSide ? [withSides.leftSide] : []
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const setDiffCommentLineColor = (diff: DiffRenderable, entry: AppliedDiffLineColor, color: DiffLineColorConfig) => {
|
|
333
|
+
for (const target of diffSideTargets(diff, entry.anchor, entry.view)) {
|
|
334
|
+
target.setLineColor(entry.anchor.localRenderLine, color)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
205
338
|
const getDetailPlaceholderContent = ({
|
|
206
339
|
status,
|
|
207
340
|
retryProgress,
|
|
@@ -248,6 +381,9 @@ export const App = () => {
|
|
|
248
381
|
const { width, height } = useTerminalDimensions()
|
|
249
382
|
const pullRequestResult = useAtomValue(pullRequestsAtom)
|
|
250
383
|
const refreshPullRequestsAtom = useAtomRefresh(pullRequestsAtom)
|
|
384
|
+
const [queueMode, setQueueMode] = useAtom(queueModeAtom)
|
|
385
|
+
const [queueLoadCache, setQueueLoadCache] = useAtom(queueLoadCacheAtom)
|
|
386
|
+
const [queueSelection, setQueueSelection] = useAtom(queueSelectionAtom)
|
|
251
387
|
const [selectedIndex, setSelectedIndex] = useAtom(selectedIndexAtom)
|
|
252
388
|
const [notice, setNotice] = useAtom(noticeAtom)
|
|
253
389
|
const [filterQuery, setFilterQuery] = useAtom(filterQueryAtom)
|
|
@@ -260,11 +396,43 @@ export const App = () => {
|
|
|
260
396
|
const [diffFileIndex, setDiffFileIndex] = useAtom(diffFileIndexAtom)
|
|
261
397
|
const [diffRenderView, setDiffRenderView] = useAtom(diffRenderViewAtom)
|
|
262
398
|
const [diffWrapMode, setDiffWrapMode] = useAtom(diffWrapModeAtom)
|
|
399
|
+
const [diffCommentMode, setDiffCommentMode] = useAtom(diffCommentModeAtom)
|
|
400
|
+
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
|
|
401
|
+
const [diffCommentThreads, setDiffCommentThreads] = useAtom(diffCommentThreadsAtom)
|
|
402
|
+
const [diffCommentsLoaded, setDiffCommentsLoaded] = useAtom(diffCommentsLoadedAtom)
|
|
263
403
|
const [pullRequestDiffCache, setPullRequestDiffCache] = useAtom(pullRequestDiffCacheAtom)
|
|
264
|
-
const [
|
|
265
|
-
const [mergeModal, setMergeModal] = useAtom(mergeModalAtom)
|
|
404
|
+
const [activeModal, setActiveModal] = useAtom(activeModalAtom)
|
|
266
405
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
267
|
-
const
|
|
406
|
+
const closeActiveModal = () => setActiveModal(initialModal)
|
|
407
|
+
const labelModalActive = Modal.$is("Label")(activeModal)
|
|
408
|
+
const closeModalActive = Modal.$is("Close")(activeModal)
|
|
409
|
+
const mergeModalActive = Modal.$is("Merge")(activeModal)
|
|
410
|
+
const commentModalActive = Modal.$is("Comment")(activeModal)
|
|
411
|
+
const commentThreadModalActive = Modal.$is("CommentThread")(activeModal)
|
|
412
|
+
const themeModalActive = Modal.$is("Theme")(activeModal)
|
|
413
|
+
const labelModal: LabelModalState = labelModalActive ? activeModal.state : initialLabelModalState
|
|
414
|
+
const closeModal: CloseModalState = closeModalActive ? activeModal.state : initialCloseModalState
|
|
415
|
+
const mergeModal: MergeModalState = mergeModalActive ? activeModal.state : initialMergeModalState
|
|
416
|
+
const commentModal: CommentModalState = commentModalActive ? activeModal.state : initialCommentModalState
|
|
417
|
+
const commentThreadModal: CommentThreadModalState = commentThreadModalActive ? activeModal.state : initialCommentThreadModalState
|
|
418
|
+
const themeModal: ThemeModalState = themeModalActive ? activeModal.state : initialThemeModalState
|
|
419
|
+
const makeModalSetter = <Tag extends Exclude<ModalTag, "None">>(tag: Tag) =>
|
|
420
|
+
(next: ModalState<Tag> | ((prev: ModalState<Tag>) => ModalState<Tag>)) => setActiveModal((current) => {
|
|
421
|
+
const ctor = Modal[tag] as (args: { state: ModalState<Tag> }) => Modal
|
|
422
|
+
if (typeof next === "function") {
|
|
423
|
+
const updater = next as (prev: ModalState<Tag>) => ModalState<Tag>
|
|
424
|
+
if (current._tag !== tag) return current
|
|
425
|
+
const prev = (current as unknown as { readonly state: ModalState<Tag> }).state
|
|
426
|
+
return ctor({ state: updater(prev) })
|
|
427
|
+
}
|
|
428
|
+
return ctor({ state: next })
|
|
429
|
+
})
|
|
430
|
+
const setLabelModal = makeModalSetter("Label")
|
|
431
|
+
const setCloseModal = makeModalSetter("Close")
|
|
432
|
+
const setMergeModal = makeModalSetter("Merge")
|
|
433
|
+
const setCommentModal = makeModalSetter("Comment")
|
|
434
|
+
const setCommentThreadModal = makeModalSetter("CommentThread")
|
|
435
|
+
const setThemeModal = makeModalSetter("Theme")
|
|
268
436
|
setActiveTheme(themeId)
|
|
269
437
|
const themeIdRef = useRef(themeId)
|
|
270
438
|
const themeModalRef = useRef(themeModal)
|
|
@@ -272,8 +440,11 @@ export const App = () => {
|
|
|
272
440
|
themeModalRef.current = themeModal
|
|
273
441
|
const [labelCache, setLabelCache] = useAtom(labelCacheAtom)
|
|
274
442
|
const [pullRequestOverrides, setPullRequestOverrides] = useAtom(pullRequestOverridesAtom)
|
|
443
|
+
const [recentlyCompletedPullRequests, setRecentlyCompletedPullRequests] = useAtom(recentlyCompletedPullRequestsAtom)
|
|
275
444
|
const retryProgress = useAtomValue(retryProgressAtom)
|
|
276
445
|
const [loadingFrame, setLoadingFrame] = useState(0)
|
|
446
|
+
const [refreshCompletionMessage, setRefreshCompletionMessage] = useState<string | null>(null)
|
|
447
|
+
const [refreshStartedAt, setRefreshStartedAt] = useState<number | null>(null)
|
|
277
448
|
const [terminalFocused, setTerminalFocused] = useState(true)
|
|
278
449
|
const usernameResult = useAtomValue(usernameAtom)
|
|
279
450
|
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
@@ -282,8 +453,11 @@ export const App = () => {
|
|
|
282
453
|
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
283
454
|
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
284
455
|
const getPullRequestDiff = useAtomSet(getPullRequestDiffAtom, { mode: "promise" })
|
|
456
|
+
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
285
457
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
286
458
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
459
|
+
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
460
|
+
const createPullRequestComment = useAtomSet(createPullRequestCommentAtom, { mode: "promise" })
|
|
287
461
|
const terminalWidth = width ?? 100
|
|
288
462
|
const terminalHeight = height ?? 24
|
|
289
463
|
const contentWidth = Math.max(1, terminalWidth)
|
|
@@ -301,6 +475,8 @@ export const App = () => {
|
|
|
301
475
|
const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
302
476
|
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
303
477
|
const detailHydrationRef = useRef<number | null>(null)
|
|
478
|
+
const refreshGenerationRef = useRef(0)
|
|
479
|
+
const didMountQueueModeRef = useRef(false)
|
|
304
480
|
const lastPullRequestRefreshAtRef = useRef(0)
|
|
305
481
|
const terminalFocusedRef = useRef(true)
|
|
306
482
|
const terminalWasBlurredRef = useRef(false)
|
|
@@ -309,6 +485,9 @@ export const App = () => {
|
|
|
309
485
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
310
486
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
311
487
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
488
|
+
const diffRenderableRefs = useRef(new Map<number, DiffRenderable>())
|
|
489
|
+
const diffCommentLineColorsRef = useRef<AppliedDiffLineColorState>({ contextKey: null, entries: [] })
|
|
490
|
+
const suppressNextDiffCommentScrollRef = useRef(false)
|
|
312
491
|
const headerFooterWidth = Math.max(24, contentWidth - 2)
|
|
313
492
|
|
|
314
493
|
const flashNotice = (message: string) => {
|
|
@@ -337,12 +516,23 @@ export const App = () => {
|
|
|
337
516
|
}
|
|
338
517
|
}, [])
|
|
339
518
|
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
519
|
+
const resolvedPullRequestLoad = AsyncResult.getOrElse(pullRequestResult, () => null)
|
|
520
|
+
const pullRequestLoad = queueLoadCache[queueMode] ?? (resolvedPullRequestLoad?.queueMode === queueMode ? resolvedPullRequestLoad : null)
|
|
521
|
+
const isLoadingQueueMode = resolvedPullRequestLoad !== null && resolvedPullRequestLoad.queueMode !== queueMode
|
|
522
|
+
const pullRequests = useMemo(() => {
|
|
523
|
+
const source = pullRequestLoad?.data ?? []
|
|
524
|
+
const seenUrls = new Set<string>()
|
|
525
|
+
const openPullRequests = source.map((pullRequest) => {
|
|
526
|
+
seenUrls.add(pullRequest.url)
|
|
527
|
+
return recentlyCompletedPullRequests[pullRequest.url] ?? pullRequestOverrides[pullRequest.url] ?? pullRequest
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
return [
|
|
531
|
+
...openPullRequests,
|
|
532
|
+
...Object.values(recentlyCompletedPullRequests).filter((pullRequest) => !seenUrls.has(pullRequest.url)),
|
|
533
|
+
]
|
|
534
|
+
}, [pullRequestLoad?.data, pullRequestOverrides, recentlyCompletedPullRequests])
|
|
535
|
+
const pullRequestStatus: LoadStatus = (pullRequestResult.waiting || isLoadingQueueMode) && pullRequestLoad === null
|
|
346
536
|
? "loading"
|
|
347
537
|
: AsyncResult.isFailure(pullRequestResult)
|
|
348
538
|
? "error"
|
|
@@ -355,18 +545,47 @@ export const App = () => {
|
|
|
355
545
|
const effectiveFilterQuery = (filterMode ? filterDraft : filterQuery).trim().toLowerCase()
|
|
356
546
|
const visibleFilterText = filterMode ? filterDraft : filterQuery
|
|
357
547
|
|
|
358
|
-
const filteredPullRequests = useMemo(() =>
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
548
|
+
const filteredPullRequests = useMemo(() => {
|
|
549
|
+
if (effectiveFilterQuery.length === 0) return pullRequests
|
|
550
|
+
return pullRequests.flatMap((pullRequest) => {
|
|
551
|
+
const score = pullRequestFilterScore(pullRequest, effectiveFilterQuery)
|
|
552
|
+
return score === null ? [] : [{ pullRequest, score }]
|
|
553
|
+
}).sort((left, right) =>
|
|
554
|
+
left.score - right.score || right.pullRequest.createdAt.getTime() - left.pullRequest.createdAt.getTime()
|
|
555
|
+
).map(({ pullRequest }) => pullRequest)
|
|
556
|
+
}, [pullRequests, effectiveFilterQuery])
|
|
557
|
+
|
|
558
|
+
const visibleRepoOrder = useMemo(() => effectiveFilterQuery.length > 0
|
|
559
|
+
? [...new Set(filteredPullRequests.map((pullRequest) => pullRequest.repository))]
|
|
560
|
+
: [], [filteredPullRequests, effectiveFilterQuery.length])
|
|
365
561
|
const visibleGroups = useMemo(
|
|
366
|
-
() => groupBy(filteredPullRequests, (pullRequest) => pullRequest.repository),
|
|
367
|
-
[filteredPullRequests],
|
|
562
|
+
() => groupBy(filteredPullRequests, (pullRequest) => pullRequest.repository, visibleRepoOrder),
|
|
563
|
+
[filteredPullRequests, visibleRepoOrder],
|
|
368
564
|
)
|
|
369
565
|
const visiblePullRequests = useMemo(() => visibleGroups.flatMap(([, pullRequests]) => pullRequests), [visibleGroups])
|
|
566
|
+
const selectedPullRequest = visiblePullRequests[selectedIndex] ?? null
|
|
567
|
+
const selectedDiffState = selectedPullRequest ? pullRequestDiffCache[pullRequestDiffKey(selectedPullRequest)] : undefined
|
|
568
|
+
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
569
|
+
const readyDiffFiles = selectedDiffState?.status === "ready" ? selectedDiffState.files : []
|
|
570
|
+
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
571
|
+
const selectedDiffKey = selectedPullRequest ? pullRequestDiffKey(selectedPullRequest) : null
|
|
572
|
+
const selectedDiffCommentCount = useMemo(() => selectedDiffKey
|
|
573
|
+
? Object.entries(diffCommentThreads)
|
|
574
|
+
.filter(([key, comments]) => key.startsWith(`${selectedDiffKey}:`) && comments.length > 0)
|
|
575
|
+
.reduce((count, [, comments]) => count + comments.length, 0)
|
|
576
|
+
: 0, [diffCommentThreads, selectedDiffKey])
|
|
577
|
+
const diffCommentAnchors = useMemo(
|
|
578
|
+
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
579
|
+
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
|
|
580
|
+
)
|
|
581
|
+
const selectedDiffCommentAnchor = diffCommentAnchors[Math.max(0, Math.min(diffCommentAnchorIndex, diffCommentAnchors.length - 1))] ?? null
|
|
582
|
+
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? `${selectedDiffKey}:${diffCommentAnchorKey(selectedDiffCommentAnchor)}` : null
|
|
583
|
+
const selectedDiffCommentThread = selectedDiffCommentThreadKey ? diffCommentThreads[selectedDiffCommentThreadKey] ?? [] : []
|
|
584
|
+
const diffLineColorContextKey = selectedDiffKey ? `${selectedDiffKey}:${effectiveDiffRenderView}:${diffWrapMode}` : null
|
|
585
|
+
const diffCommentRows = useMemo(
|
|
586
|
+
() => [...new Set(diffCommentAnchors.map((anchor) => anchor.renderLine))].sort((left, right) => left - right),
|
|
587
|
+
[diffCommentAnchors],
|
|
588
|
+
)
|
|
370
589
|
const groupStarts = useMemo(() => visibleGroups.reduce<Array<number>>((starts, [, pullRequests], index) => {
|
|
371
590
|
if (index === 0) {
|
|
372
591
|
starts.push(0)
|
|
@@ -386,12 +605,15 @@ export const App = () => {
|
|
|
386
605
|
: pullRequestStatus === "loading"
|
|
387
606
|
? "loading pull requests..."
|
|
388
607
|
: ""
|
|
389
|
-
const headerLeft = username ? `GHUI ${username}` :
|
|
608
|
+
const headerLeft = username ? `GHUI ${username} ${pullRequestQueueLabels[queueMode]}` : `GHUI ${pullRequestQueueLabels[queueMode]}`
|
|
390
609
|
const headerLine = `${fitCell(headerLeft, Math.max(0, headerFooterWidth - summaryRight.length))}${summaryRight}`
|
|
391
610
|
const footerNotice = notice ? fitCell(notice, headerFooterWidth) : null
|
|
392
611
|
const selectPullRequestByUrl = (url: string) => {
|
|
393
612
|
const index = visiblePullRequests.findIndex((pullRequest) => pullRequest.url === url)
|
|
394
|
-
if (index >= 0)
|
|
613
|
+
if (index >= 0) {
|
|
614
|
+
setSelectedIndex(index)
|
|
615
|
+
setQueueSelection((current) => ({ ...current, [queueMode]: index }))
|
|
616
|
+
}
|
|
395
617
|
}
|
|
396
618
|
const updatePullRequest = (url: string, transform: (pullRequest: PullRequestItem) => PullRequestItem) => {
|
|
397
619
|
const pullRequest = pullRequests.find((item) => item.url === url)
|
|
@@ -399,10 +621,33 @@ export const App = () => {
|
|
|
399
621
|
setPullRequestOverrides((current) => ({ ...current, [url]: transform(pullRequest) }))
|
|
400
622
|
}
|
|
401
623
|
const refreshPullRequests = (message?: string) => {
|
|
624
|
+
refreshGenerationRef.current += 1
|
|
625
|
+
setPullRequestOverrides({})
|
|
626
|
+
if (message) {
|
|
627
|
+
setNotice(null)
|
|
628
|
+
setRefreshCompletionMessage(message)
|
|
629
|
+
setRefreshStartedAt(lastPullRequestRefreshAtRef.current)
|
|
630
|
+
}
|
|
402
631
|
refreshPullRequestsAtom()
|
|
403
|
-
if (message) flashNotice(message)
|
|
404
632
|
}
|
|
405
633
|
refreshPullRequestsRef.current = refreshPullRequests
|
|
634
|
+
const switchQueueMode = (delta: 1 | -1) => {
|
|
635
|
+
const mode = nextQueueMode(queueMode, delta)
|
|
636
|
+
if (mode === queueMode) return
|
|
637
|
+
refreshGenerationRef.current += 1
|
|
638
|
+
setQueueSelection((current) => ({ ...current, [queueMode]: selectedIndex }))
|
|
639
|
+
setQueueMode(mode)
|
|
640
|
+
setSelectedIndex(queueSelection[mode] ?? 0)
|
|
641
|
+
setRecentlyCompletedPullRequests({})
|
|
642
|
+
detailHydrationRef.current = null
|
|
643
|
+
setDetailFullView(false)
|
|
644
|
+
setDiffFullView(false)
|
|
645
|
+
setDiffCommentMode(false)
|
|
646
|
+
setFilterDraft(filterQuery)
|
|
647
|
+
setNotice(null)
|
|
648
|
+
setRefreshCompletionMessage(null)
|
|
649
|
+
setRefreshStartedAt(null)
|
|
650
|
+
}
|
|
406
651
|
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
407
652
|
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
408
653
|
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
@@ -417,6 +662,30 @@ export const App = () => {
|
|
|
417
662
|
}
|
|
418
663
|
}, [pullRequestLoad?.fetchedAt])
|
|
419
664
|
|
|
665
|
+
useEffect(() => {
|
|
666
|
+
if (!didMountQueueModeRef.current) {
|
|
667
|
+
didMountQueueModeRef.current = true
|
|
668
|
+
return
|
|
669
|
+
}
|
|
670
|
+
if (queueLoadCache[queueMode]) return
|
|
671
|
+
refreshPullRequestsAtom()
|
|
672
|
+
}, [queueMode, queueLoadCache, refreshPullRequestsAtom])
|
|
673
|
+
|
|
674
|
+
useEffect(() => {
|
|
675
|
+
if (!refreshCompletionMessage || refreshStartedAt === null) return
|
|
676
|
+
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
677
|
+
const isHydratingDetails = pullRequestStatus === "ready" && pullRequests.some((pullRequest) => pullRequest.state === "open" && !pullRequest.detailLoaded)
|
|
678
|
+
if (pullRequestStatus === "ready" && fetchedAt !== undefined && fetchedAt !== refreshStartedAt && !isHydratingDetails) {
|
|
679
|
+
flashNotice(`✓ ${refreshCompletionMessage}`)
|
|
680
|
+
setRefreshCompletionMessage(null)
|
|
681
|
+
setRefreshStartedAt(null)
|
|
682
|
+
} else if (pullRequestStatus === "error") {
|
|
683
|
+
flashNotice("Refresh failed")
|
|
684
|
+
setRefreshCompletionMessage(null)
|
|
685
|
+
setRefreshStartedAt(null)
|
|
686
|
+
}
|
|
687
|
+
}, [refreshCompletionMessage, refreshStartedAt, pullRequestStatus, pullRequestLoad?.fetchedAt, pullRequests])
|
|
688
|
+
|
|
420
689
|
useEffect(() => {
|
|
421
690
|
const handleFocus = () => {
|
|
422
691
|
terminalFocusedRef.current = true
|
|
@@ -457,15 +726,72 @@ export const App = () => {
|
|
|
457
726
|
})
|
|
458
727
|
}, [visiblePullRequests.length])
|
|
459
728
|
|
|
729
|
+
useEffect(() => {
|
|
730
|
+
setQueueSelection((current) => current[queueMode] === selectedIndex ? current : { ...current, [queueMode]: selectedIndex })
|
|
731
|
+
}, [queueMode, selectedIndex])
|
|
732
|
+
|
|
460
733
|
useEffect(() => {
|
|
461
734
|
setDiffFileIndex(0)
|
|
735
|
+
setDiffCommentAnchorIndex(0)
|
|
462
736
|
}, [selectedIndex])
|
|
463
737
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
738
|
+
useEffect(() => {
|
|
739
|
+
setDiffCommentAnchorIndex((current) => {
|
|
740
|
+
if (diffCommentAnchors.length === 0) return 0
|
|
741
|
+
return Math.max(0, Math.min(current, diffCommentAnchors.length - 1))
|
|
742
|
+
})
|
|
743
|
+
}, [diffCommentAnchors.length])
|
|
744
|
+
|
|
745
|
+
useEffect(() => {
|
|
746
|
+
if (!diffCommentMode || !selectedDiffCommentAnchor) return
|
|
747
|
+
setDiffFileIndex((current) => current === selectedDiffCommentAnchor.fileIndex ? current : selectedDiffCommentAnchor.fileIndex)
|
|
748
|
+
}, [diffCommentMode, selectedDiffCommentAnchor?.fileIndex])
|
|
749
|
+
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
const previous = diffCommentLineColorsRef.current
|
|
752
|
+
if (previous.contextKey === diffLineColorContextKey) {
|
|
753
|
+
for (const entry of previous.entries) {
|
|
754
|
+
const diff = diffRenderableRefs.current.get(entry.anchor.fileIndex)
|
|
755
|
+
if (diff) setDiffCommentLineColor(diff, entry, originalDiffLineColor(entry.anchor))
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const nextEntries: AppliedDiffLineColor[] = []
|
|
760
|
+
const appliedKeys = new Set<string>()
|
|
761
|
+
const applyLineColor = (anchor: StackedDiffCommentAnchor, gutter: string, override = false) => {
|
|
762
|
+
const key = `${effectiveDiffRenderView}:${anchor.side}:${anchor.renderLine}`
|
|
763
|
+
if (appliedKeys.has(key) && !override) return
|
|
764
|
+
appliedKeys.add(key)
|
|
765
|
+
const entry = { anchor, view: effectiveDiffRenderView } satisfies AppliedDiffLineColor
|
|
766
|
+
const diff = diffRenderableRefs.current.get(anchor.fileIndex)
|
|
767
|
+
if (diff) setDiffCommentLineColor(diff, entry, { ...originalDiffLineColor(anchor), gutter })
|
|
768
|
+
if (!nextEntries.some((existing) => existing.view === entry.view && existing.anchor.side === anchor.side && existing.anchor.renderLine === anchor.renderLine)) {
|
|
769
|
+
nextEntries.push(entry)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (selectedDiffKey) {
|
|
774
|
+
for (const anchor of diffCommentAnchors) {
|
|
775
|
+
if ((diffCommentThreads[`${selectedDiffKey}:${diffCommentAnchorKey(anchor)}`]?.length ?? 0) > 0) {
|
|
776
|
+
applyLineColor(anchor, colors.status.pending)
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (diffCommentMode && selectedDiffCommentAnchor) {
|
|
781
|
+
applyLineColor(selectedDiffCommentAnchor, selectedDiffCommentAnchor.side === "RIGHT" ? colors.status.passing : colors.status.failing, true)
|
|
782
|
+
if (suppressNextDiffCommentScrollRef.current) {
|
|
783
|
+
suppressNextDiffCommentScrollRef.current = false
|
|
784
|
+
} else {
|
|
785
|
+
ensureDiffLineVisible(selectedDiffCommentAnchor.renderLine)
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
suppressNextDiffCommentScrollRef.current = false
|
|
789
|
+
}
|
|
790
|
+
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
791
|
+
}, [diffCommentMode, selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, diffLineColorContextKey, effectiveDiffRenderView, diffCommentAnchors, diffCommentThreads])
|
|
792
|
+
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && pullRequests.some((pullRequest) => pullRequest.state === "open" && !pullRequest.detailLoaded)
|
|
793
|
+
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
794
|
+
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?.status === "loading"
|
|
469
795
|
const loadingIndicator = LOADING_FRAMES[loadingFrame % LOADING_FRAMES.length]!
|
|
470
796
|
|
|
471
797
|
useEffect(() => {
|
|
@@ -479,21 +805,29 @@ export const App = () => {
|
|
|
479
805
|
useEffect(() => {
|
|
480
806
|
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
481
807
|
if (pullRequestStatus !== "ready" || fetchedAt === undefined) return
|
|
482
|
-
if (detailHydrationRef.current === fetchedAt) return
|
|
483
|
-
if (!pullRequests.some((pullRequest) => !pullRequest.detailLoaded)) return
|
|
808
|
+
if (detailHydrationRef.current === fetchedAt || pullRequestLoad?.detailsFetchedAt?.getTime() === fetchedAt) return
|
|
809
|
+
if (!pullRequests.some((pullRequest) => pullRequest.state === "open" && !pullRequest.detailLoaded)) return
|
|
484
810
|
detailHydrationRef.current = fetchedAt
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
811
|
+
const generation = refreshGenerationRef.current
|
|
812
|
+
void loadPullRequestDetails(queueMode).then((details) => {
|
|
813
|
+
if (generation !== refreshGenerationRef.current) return
|
|
814
|
+
setQueueLoadCache((current) => {
|
|
815
|
+
const load = current[queueMode]
|
|
816
|
+
if (!load) return current
|
|
817
|
+
const detailsByUrl = new Map(details.map((detail) => [detail.url, detail]))
|
|
818
|
+
return {
|
|
819
|
+
...current,
|
|
820
|
+
[queueMode]: {
|
|
821
|
+
...load,
|
|
822
|
+
data: load.data.map((pullRequest) => detailsByUrl.get(pullRequest.url) ?? pullRequest),
|
|
823
|
+
detailsFetchedAt: load.fetchedAt,
|
|
824
|
+
},
|
|
490
825
|
}
|
|
491
|
-
return next
|
|
492
826
|
})
|
|
493
827
|
}).catch((error) => {
|
|
494
|
-
flashNotice(
|
|
828
|
+
flashNotice(errorMessage(error))
|
|
495
829
|
})
|
|
496
|
-
}, [pullRequestStatus, pullRequestLoad?.fetchedAt, pullRequests.length])
|
|
830
|
+
}, [queueMode, pullRequestStatus, pullRequestLoad?.fetchedAt, pullRequests.length])
|
|
497
831
|
|
|
498
832
|
const detailPlaceholderContent = getDetailPlaceholderContent({
|
|
499
833
|
status: pullRequestStatus,
|
|
@@ -506,9 +840,53 @@ export const App = () => {
|
|
|
506
840
|
|
|
507
841
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
508
842
|
|
|
509
|
-
const
|
|
843
|
+
const loadPullRequestComments = (pullRequest: PullRequestItem, force = false) => {
|
|
844
|
+
const key = pullRequestDiffKey(pullRequest)
|
|
845
|
+
const previousLoadState = diffCommentsLoaded[key]
|
|
846
|
+
if (!force && previousLoadState) return
|
|
847
|
+
setDiffCommentsLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
848
|
+
void listPullRequestComments({ repository: pullRequest.repository, number: pullRequest.number })
|
|
849
|
+
.then((comments) => {
|
|
850
|
+
setDiffCommentsLoaded((current) => ({ ...current, [key]: "ready" }))
|
|
851
|
+
setDiffCommentThreads((current) => {
|
|
852
|
+
const prefix = `${key}:`
|
|
853
|
+
const threads = groupDiffCommentThreads(pullRequest, comments)
|
|
854
|
+
const next: Record<string, readonly PullRequestReviewComment[]> = Object.fromEntries(
|
|
855
|
+
Object.entries(current).filter(([threadKey]) => !threadKey.startsWith(prefix)),
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
for (const [threadKey, threadComments] of Object.entries(current)) {
|
|
859
|
+
if (!threadKey.startsWith(prefix)) continue
|
|
860
|
+
const localComments = threadComments.filter(isLocalDiffComment)
|
|
861
|
+
if (localComments.length > 0) {
|
|
862
|
+
next[threadKey] = [...(threads[threadKey] ?? []), ...localComments]
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
for (const [threadKey, threadComments] of Object.entries(threads)) {
|
|
867
|
+
if (!next[threadKey]) next[threadKey] = threadComments
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return next
|
|
871
|
+
})
|
|
872
|
+
})
|
|
873
|
+
.catch((error) => {
|
|
874
|
+
setDiffCommentsLoaded((current) => {
|
|
875
|
+
if (previousLoadState === "ready") return { ...current, [key]: previousLoadState }
|
|
876
|
+
const next = { ...current }
|
|
877
|
+
delete next[key]
|
|
878
|
+
return next
|
|
879
|
+
})
|
|
880
|
+
flashNotice(errorMessage(error))
|
|
881
|
+
})
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const loadPullRequestDiff = (pullRequest: PullRequestItem, options: { readonly force?: boolean; readonly includeComments?: boolean } = {}) => {
|
|
885
|
+
const force = options.force ?? false
|
|
886
|
+
const includeComments = options.includeComments ?? false
|
|
510
887
|
const key = pullRequestDiffKey(pullRequest)
|
|
511
888
|
const existing = pullRequestDiffCache[key]
|
|
889
|
+
if (includeComments) loadPullRequestComments(pullRequest, force)
|
|
512
890
|
if (!force && (existing?.status === "ready" || existing?.status === "loading")) return
|
|
513
891
|
|
|
514
892
|
setPullRequestDiffCache((current) => ({ ...current, [key]: { status: "loading" } }))
|
|
@@ -546,12 +924,168 @@ export const App = () => {
|
|
|
546
924
|
|
|
547
925
|
const openDiffView = () => {
|
|
548
926
|
if (!selectedPullRequest) return
|
|
927
|
+
diffRenderableRefs.current.clear()
|
|
928
|
+
diffCommentLineColorsRef.current = { contextKey: null, entries: [] }
|
|
549
929
|
setDiffFullView(true)
|
|
550
930
|
setDetailFullView(false)
|
|
931
|
+
setDiffCommentMode(false)
|
|
551
932
|
setDiffFileIndex(0)
|
|
552
933
|
setDiffRenderView(contentWidth >= 100 ? "split" : "unified")
|
|
553
934
|
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
554
|
-
loadPullRequestDiff(selectedPullRequest)
|
|
935
|
+
loadPullRequestDiff(selectedPullRequest, { includeComments: true })
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const setDiffRenderableRef = (index: number, diff: DiffRenderable | null) => {
|
|
939
|
+
if (diff) diffRenderableRefs.current.set(index, diff)
|
|
940
|
+
else diffRenderableRefs.current.delete(index)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const scrollToDiffFile = (index: number) => {
|
|
944
|
+
const stackedFile = stackedDiffFiles[index]
|
|
945
|
+
diffScrollRef.current?.scrollTo({ x: 0, y: stackedFile?.headerLine ?? 0 })
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const syncDiffFileIndexToScroll = () => {
|
|
949
|
+
const scrollTop = diffScrollRef.current?.scrollTop
|
|
950
|
+
if (scrollTop === undefined || stackedDiffFiles.length === 0) return
|
|
951
|
+
const nextIndex = stackedDiffFiles.reduce((current, file) => file.headerLine <= scrollTop + 1 ? file.index : current, 0)
|
|
952
|
+
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const scrollDiffBy = (y: number) => {
|
|
956
|
+
diffScrollRef.current?.scrollBy({ x: 0, y })
|
|
957
|
+
syncDiffFileIndexToScroll()
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const scrollDiffTo = (y: number) => {
|
|
961
|
+
diffScrollRef.current?.scrollTo({ x: 0, y })
|
|
962
|
+
if (y <= 0) setDiffFileIndex(0)
|
|
963
|
+
else if (y === Number.MAX_SAFE_INTEGER) setDiffFileIndex(Math.max(0, readyDiffFiles.length - 1))
|
|
964
|
+
else syncDiffFileIndexToScroll()
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const ensureDiffLineVisible = (line: number) => {
|
|
968
|
+
const scroll = diffScrollRef.current
|
|
969
|
+
if (!scroll) return
|
|
970
|
+
const viewportHeight = Math.max(1, wideBodyHeight - (selectedDiffCommentThread.length > 0 ? 6 : 3))
|
|
971
|
+
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, line)
|
|
972
|
+
if (nextTop !== scroll.scrollTop) scroll.scrollTo({ x: 0, y: nextTop })
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const jumpDiffFile = (delta: 1 | -1) => {
|
|
976
|
+
if (readyDiffFiles.length === 0) return
|
|
977
|
+
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
978
|
+
setDiffFileIndex(nextIndex)
|
|
979
|
+
if (diffCommentMode) {
|
|
980
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex && anchor.side === selectedDiffCommentAnchor?.side)
|
|
981
|
+
?? diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex)
|
|
982
|
+
if (nextAnchor) setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
983
|
+
}
|
|
984
|
+
scrollToDiffFile(nextIndex)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const enterDiffCommentMode = () => {
|
|
988
|
+
const scrollTop = diffScrollRef.current?.scrollTop ?? 0
|
|
989
|
+
suppressNextDiffCommentScrollRef.current = true
|
|
990
|
+
setDiffCommentAnchorIndex(nearestDiffCommentAnchorIndex(diffCommentAnchors, scrollTop))
|
|
991
|
+
setDiffCommentMode(true)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const moveDiffCommentAnchor = (delta: number) => {
|
|
995
|
+
if (diffCommentAnchors.length === 0) return
|
|
996
|
+
const currentAnchor = selectedDiffCommentAnchor ?? diffCommentAnchors[0]
|
|
997
|
+
const currentRowIndex = Math.max(0, currentAnchor ? diffCommentRows.indexOf(currentAnchor.renderLine) : 0)
|
|
998
|
+
const nextRow = diffCommentRows[Math.max(0, Math.min(diffCommentRows.length - 1, currentRowIndex + delta))]
|
|
999
|
+
if (nextRow === undefined) return
|
|
1000
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.renderLine === nextRow && anchor.side === currentAnchor?.side)
|
|
1001
|
+
?? diffCommentAnchors.find((anchor) => anchor.renderLine === nextRow)
|
|
1002
|
+
if (!nextAnchor) return
|
|
1003
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const selectDiffCommentSide = (side: DiffCommentSide) => {
|
|
1007
|
+
if (!selectedDiffCommentAnchor) return
|
|
1008
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.renderLine === selectedDiffCommentAnchor.renderLine && anchor.side === side)
|
|
1009
|
+
if (!nextAnchor) return
|
|
1010
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const editComment = (transform: (state: CommentEditorValue) => CommentEditorValue) => {
|
|
1014
|
+
setCommentModal((current) => {
|
|
1015
|
+
const next = transform({ body: current.body, cursor: current.cursor })
|
|
1016
|
+
if (next.body === current.body && next.cursor === current.cursor && current.error === null) return current
|
|
1017
|
+
return { ...current, body: next.body, cursor: next.cursor, error: null }
|
|
1018
|
+
})
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const openDiffCommentModal = () => {
|
|
1022
|
+
if (!selectedDiffCommentAnchor || !selectedPullRequest) return
|
|
1023
|
+
setCommentModal(initialCommentModalState)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const openDiffCommentThreadModal = () => {
|
|
1027
|
+
if (!selectedDiffCommentAnchor || selectedDiffCommentThread.length === 0) return
|
|
1028
|
+
setCommentThreadModal({ scrollOffset: 0 })
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const submitDiffComment = () => {
|
|
1032
|
+
if (!selectedPullRequest || !selectedDiffCommentAnchor) return
|
|
1033
|
+
const body = commentModal.body.trim()
|
|
1034
|
+
if (body.length === 0) {
|
|
1035
|
+
setCommentModal((current) => ({ ...current, error: "Write a comment before saving." }))
|
|
1036
|
+
return
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const threadKey = selectedDiffCommentThreadKey
|
|
1040
|
+
const target = selectedDiffCommentAnchor
|
|
1041
|
+
const optimisticComment = {
|
|
1042
|
+
id: `local:${Date.now()}`,
|
|
1043
|
+
path: target.path,
|
|
1044
|
+
line: target.line,
|
|
1045
|
+
side: target.side,
|
|
1046
|
+
author: username ?? "you",
|
|
1047
|
+
body,
|
|
1048
|
+
createdAt: new Date(),
|
|
1049
|
+
url: null,
|
|
1050
|
+
} satisfies PullRequestReviewComment
|
|
1051
|
+
const input = {
|
|
1052
|
+
repository: selectedPullRequest.repository,
|
|
1053
|
+
number: selectedPullRequest.number,
|
|
1054
|
+
commitId: selectedPullRequest.headRefOid,
|
|
1055
|
+
path: target.path,
|
|
1056
|
+
line: target.line,
|
|
1057
|
+
side: target.side,
|
|
1058
|
+
body,
|
|
1059
|
+
} satisfies CreatePullRequestCommentInput
|
|
1060
|
+
|
|
1061
|
+
if (threadKey) {
|
|
1062
|
+
setDiffCommentThreads((current) => ({
|
|
1063
|
+
...current,
|
|
1064
|
+
[threadKey]: [...(current[threadKey] ?? []), optimisticComment],
|
|
1065
|
+
}))
|
|
1066
|
+
}
|
|
1067
|
+
closeActiveModal()
|
|
1068
|
+
flashNotice(`Commenting on ${target.path}:${target.line}`)
|
|
1069
|
+
void createPullRequestComment(input).then((comment) => {
|
|
1070
|
+
if (threadKey) {
|
|
1071
|
+
setDiffCommentThreads((current) => ({
|
|
1072
|
+
...current,
|
|
1073
|
+
[threadKey]: (current[threadKey] ?? []).map((existing) => existing.id === optimisticComment.id ? comment : existing),
|
|
1074
|
+
}))
|
|
1075
|
+
}
|
|
1076
|
+
flashNotice(`Commented on ${target.path}:${target.line}`)
|
|
1077
|
+
}).catch((error) => {
|
|
1078
|
+
if (threadKey) {
|
|
1079
|
+
setDiffCommentThreads((current) => {
|
|
1080
|
+
const next = { ...current }
|
|
1081
|
+
const comments = (next[threadKey] ?? []).filter((comment) => comment.id !== optimisticComment.id)
|
|
1082
|
+
if (comments.length > 0) next[threadKey] = comments
|
|
1083
|
+
else delete next[threadKey]
|
|
1084
|
+
return next
|
|
1085
|
+
})
|
|
1086
|
+
}
|
|
1087
|
+
flashNotice(errorMessage(error))
|
|
1088
|
+
})
|
|
555
1089
|
}
|
|
556
1090
|
|
|
557
1091
|
const openSelectedPullRequestInBrowser = (pullRequest: PullRequestItem) => {
|
|
@@ -560,11 +1094,73 @@ export const App = () => {
|
|
|
560
1094
|
.catch((error) => flashNotice(errorMessage(error)))
|
|
561
1095
|
}
|
|
562
1096
|
|
|
1097
|
+
const copySelectedPullRequestMetadata = () => {
|
|
1098
|
+
if (!selectedPullRequest) return
|
|
1099
|
+
void copyPullRequestMetadata(selectedPullRequest)
|
|
1100
|
+
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1101
|
+
.catch((error) => flashNotice(errorMessage(error)))
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const toggleSelectedPullRequestDraftStatus = () => {
|
|
1105
|
+
if (!selectedPullRequest) return
|
|
1106
|
+
const previousPullRequest = selectedPullRequest
|
|
1107
|
+
const nextReviewStatus = selectedPullRequest.reviewStatus === "draft" ? "review" : "draft"
|
|
1108
|
+
updatePullRequest(selectedPullRequest.url, (pullRequest) => ({
|
|
1109
|
+
...pullRequest,
|
|
1110
|
+
reviewStatus: nextReviewStatus,
|
|
1111
|
+
}))
|
|
1112
|
+
void toggleDraftStatus({ repository: selectedPullRequest.repository, number: selectedPullRequest.number, isDraft: selectedPullRequest.reviewStatus === "draft" })
|
|
1113
|
+
.then(() => {
|
|
1114
|
+
flashNotice(selectedPullRequest.reviewStatus === "draft" ? `Marked #${selectedPullRequest.number} ready` : `Marked #${selectedPullRequest.number} draft`)
|
|
1115
|
+
})
|
|
1116
|
+
.catch((error) => {
|
|
1117
|
+
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
1118
|
+
flashNotice(errorMessage(error))
|
|
1119
|
+
})
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const openCloseModal = () => {
|
|
1123
|
+
if (!selectedPullRequest || selectedPullRequest.state !== "open") return
|
|
1124
|
+
setCloseModal({
|
|
1125
|
+
repository: selectedPullRequest.repository,
|
|
1126
|
+
number: selectedPullRequest.number,
|
|
1127
|
+
title: selectedPullRequest.title,
|
|
1128
|
+
url: selectedPullRequest.url,
|
|
1129
|
+
running: false,
|
|
1130
|
+
error: null,
|
|
1131
|
+
})
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const confirmClosePullRequest = () => {
|
|
1135
|
+
if (!closeModal.repository || closeModal.number === null || !closeModal.url || closeModal.running) return
|
|
1136
|
+
const { repository, number, url } = closeModal
|
|
1137
|
+
const targetPullRequest = pullRequests.find((pullRequest) => pullRequest.url === url)
|
|
1138
|
+
const previousPullRequest = targetPullRequest ?? null
|
|
1139
|
+
|
|
1140
|
+
setCloseModal((current) => ({ ...current, running: true, error: null }))
|
|
1141
|
+
void closePullRequest({ repository, number })
|
|
1142
|
+
.then(() => {
|
|
1143
|
+
if (previousPullRequest) {
|
|
1144
|
+
setRecentlyCompletedPullRequests((current) => ({
|
|
1145
|
+
...current,
|
|
1146
|
+
[previousPullRequest.url]: {
|
|
1147
|
+
...previousPullRequest,
|
|
1148
|
+
state: "closed",
|
|
1149
|
+
autoMergeEnabled: false,
|
|
1150
|
+
},
|
|
1151
|
+
}))
|
|
1152
|
+
}
|
|
1153
|
+
closeActiveModal()
|
|
1154
|
+
refreshPullRequests(`Closed #${number}`)
|
|
1155
|
+
})
|
|
1156
|
+
.catch((error) => {
|
|
1157
|
+
setCloseModal((current) => ({ ...current, running: false, error: errorMessage(error) }))
|
|
1158
|
+
flashNotice(errorMessage(error))
|
|
1159
|
+
})
|
|
1160
|
+
}
|
|
1161
|
+
|
|
563
1162
|
const openThemeModal = () => {
|
|
564
|
-
setLabelModal(initialLabelModalState)
|
|
565
|
-
setMergeModal(initialMergeModalState)
|
|
566
1163
|
setThemeModal({
|
|
567
|
-
open: true,
|
|
568
1164
|
query: "",
|
|
569
1165
|
filterMode: false,
|
|
570
1166
|
initialThemeId: themeId,
|
|
@@ -579,7 +1175,7 @@ export const App = () => {
|
|
|
579
1175
|
void Effect.runPromise(saveStoredThemeId(selectedTheme.id)).catch((error) => flashNotice(errorMessage(error)))
|
|
580
1176
|
flashNotice(`Theme: ${selectedTheme.name}`)
|
|
581
1177
|
}
|
|
582
|
-
|
|
1178
|
+
closeActiveModal()
|
|
583
1179
|
}
|
|
584
1180
|
|
|
585
1181
|
const previewTheme = (id: ThemeId) => {
|
|
@@ -622,13 +1218,10 @@ export const App = () => {
|
|
|
622
1218
|
|
|
623
1219
|
const openLabelModal = () => {
|
|
624
1220
|
if (!selectedPullRequest) return
|
|
625
|
-
setMergeModal(initialMergeModalState)
|
|
626
|
-
setThemeModal(initialThemeModalState)
|
|
627
1221
|
const repository = selectedPullRequest.repository
|
|
628
1222
|
const cachedLabels = labelCache[repository]
|
|
629
1223
|
if (cachedLabels) {
|
|
630
1224
|
setLabelModal({
|
|
631
|
-
open: true,
|
|
632
1225
|
repository,
|
|
633
1226
|
query: "",
|
|
634
1227
|
selectedIndex: 0,
|
|
@@ -638,7 +1231,7 @@ export const App = () => {
|
|
|
638
1231
|
return
|
|
639
1232
|
}
|
|
640
1233
|
|
|
641
|
-
setLabelModal(
|
|
1234
|
+
setLabelModal({ repository, query: "", selectedIndex: 0, availableLabels: [], loading: true })
|
|
642
1235
|
void loadRepoLabels(repository)
|
|
643
1236
|
.then((labels) => {
|
|
644
1237
|
setLabelCache((current) => ({ ...current, [repository]: labels }))
|
|
@@ -646,19 +1239,16 @@ export const App = () => {
|
|
|
646
1239
|
})
|
|
647
1240
|
.catch((error) => {
|
|
648
1241
|
setLabelModal((current) => current.repository === repository ? { ...current, loading: false } : current)
|
|
649
|
-
flashNotice(
|
|
1242
|
+
flashNotice(errorMessage(error))
|
|
650
1243
|
})
|
|
651
1244
|
}
|
|
652
1245
|
|
|
653
1246
|
const openMergeModal = () => {
|
|
654
1247
|
if (!selectedPullRequest) return
|
|
655
|
-
setThemeModal(initialThemeModalState)
|
|
656
1248
|
const repository = selectedPullRequest.repository
|
|
657
1249
|
const number = selectedPullRequest.number
|
|
658
1250
|
const seededInfo = mergeInfoFromPullRequest(selectedPullRequest)
|
|
659
|
-
setLabelModal(initialLabelModalState)
|
|
660
1251
|
setMergeModal({
|
|
661
|
-
open: true,
|
|
662
1252
|
repository,
|
|
663
1253
|
number,
|
|
664
1254
|
selectedIndex: 0,
|
|
@@ -702,7 +1292,17 @@ export const App = () => {
|
|
|
702
1292
|
setMergeModal((current) => ({ ...current, running: true, error: null }))
|
|
703
1293
|
void mergePullRequest({ repository, number, action: option.action })
|
|
704
1294
|
.then(() => {
|
|
705
|
-
|
|
1295
|
+
if (option.refreshOnSuccess && previousPullRequest) {
|
|
1296
|
+
setRecentlyCompletedPullRequests((current) => ({
|
|
1297
|
+
...current,
|
|
1298
|
+
[previousPullRequest.url]: {
|
|
1299
|
+
...previousPullRequest,
|
|
1300
|
+
state: "merged",
|
|
1301
|
+
autoMergeEnabled: false,
|
|
1302
|
+
},
|
|
1303
|
+
}))
|
|
1304
|
+
}
|
|
1305
|
+
closeActiveModal()
|
|
706
1306
|
if (option.refreshOnSuccess) {
|
|
707
1307
|
refreshPullRequests(`${option.pastTense} #${number}`)
|
|
708
1308
|
} else {
|
|
@@ -736,7 +1336,7 @@ export const App = () => {
|
|
|
736
1336
|
.then(() => flashNotice(`Removed ${label.name} from #${selectedPullRequest.number}`))
|
|
737
1337
|
.catch((error) => {
|
|
738
1338
|
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
739
|
-
flashNotice(
|
|
1339
|
+
flashNotice(errorMessage(error))
|
|
740
1340
|
})
|
|
741
1341
|
} else {
|
|
742
1342
|
updatePullRequest(selectedPullRequest.url, (pr) => ({
|
|
@@ -747,30 +1347,26 @@ export const App = () => {
|
|
|
747
1347
|
.then(() => flashNotice(`Added ${label.name} to #${selectedPullRequest.number}`))
|
|
748
1348
|
.catch((error) => {
|
|
749
1349
|
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
750
|
-
flashNotice(
|
|
1350
|
+
flashNotice(errorMessage(error))
|
|
751
1351
|
})
|
|
752
1352
|
}
|
|
753
1353
|
}
|
|
754
1354
|
|
|
755
1355
|
useKeyboard((key) => {
|
|
756
|
-
if ((key.name === "q" && !(
|
|
757
|
-
if (
|
|
1356
|
+
if ((key.name === "q" && !commentModalActive && !(themeModalActive && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
|
|
1357
|
+
if (themeModalActive) {
|
|
758
1358
|
closeThemeModal(false)
|
|
759
1359
|
return
|
|
760
1360
|
}
|
|
761
|
-
if (
|
|
762
|
-
|
|
763
|
-
return
|
|
764
|
-
}
|
|
765
|
-
if (labelModal.open) {
|
|
766
|
-
setLabelModal(initialLabelModalState)
|
|
1361
|
+
if (activeModal._tag !== "None") {
|
|
1362
|
+
closeActiveModal()
|
|
767
1363
|
return
|
|
768
1364
|
}
|
|
769
1365
|
renderer.destroy()
|
|
770
1366
|
return
|
|
771
1367
|
}
|
|
772
1368
|
|
|
773
|
-
if (
|
|
1369
|
+
if (themeModalActive) {
|
|
774
1370
|
if (key.name === "escape") {
|
|
775
1371
|
if (themeModal.filterMode) {
|
|
776
1372
|
updateThemeQuery("", { filterMode: false })
|
|
@@ -811,10 +1407,150 @@ export const App = () => {
|
|
|
811
1407
|
return
|
|
812
1408
|
}
|
|
813
1409
|
|
|
814
|
-
if (
|
|
1410
|
+
if (commentModalActive) {
|
|
1411
|
+
if (key.name === "escape") {
|
|
1412
|
+
closeActiveModal()
|
|
1413
|
+
return
|
|
1414
|
+
}
|
|
1415
|
+
if (key.ctrl && key.name === "s") {
|
|
1416
|
+
submitDiffComment()
|
|
1417
|
+
return
|
|
1418
|
+
}
|
|
1419
|
+
if (key.ctrl && key.name === "a") {
|
|
1420
|
+
editComment(moveLineStart)
|
|
1421
|
+
return
|
|
1422
|
+
}
|
|
1423
|
+
if (key.ctrl && key.name === "e") {
|
|
1424
|
+
editComment(moveLineEnd)
|
|
1425
|
+
return
|
|
1426
|
+
}
|
|
1427
|
+
if (key.ctrl && key.name === "b") {
|
|
1428
|
+
editComment(editorMoveLeft)
|
|
1429
|
+
return
|
|
1430
|
+
}
|
|
1431
|
+
if (key.ctrl && key.name === "f") {
|
|
1432
|
+
editComment(editorMoveRight)
|
|
1433
|
+
return
|
|
1434
|
+
}
|
|
1435
|
+
if (key.ctrl && key.name === "w") {
|
|
1436
|
+
editComment(deleteWordBackward)
|
|
1437
|
+
return
|
|
1438
|
+
}
|
|
1439
|
+
if (key.ctrl && key.name === "u") {
|
|
1440
|
+
editComment(deleteToLineStart)
|
|
1441
|
+
return
|
|
1442
|
+
}
|
|
1443
|
+
if (key.ctrl && key.name === "k") {
|
|
1444
|
+
editComment(deleteToLineEnd)
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
if (key.ctrl && key.name === "d") {
|
|
1448
|
+
editComment(editorDeleteForward)
|
|
1449
|
+
return
|
|
1450
|
+
}
|
|
1451
|
+
if ((key.meta || key.option) && (key.name === "b" || key.name === "left")) {
|
|
1452
|
+
editComment(moveWordBackward)
|
|
1453
|
+
return
|
|
1454
|
+
}
|
|
1455
|
+
if ((key.meta || key.option) && (key.name === "f" || key.name === "right")) {
|
|
1456
|
+
editComment(moveWordForward)
|
|
1457
|
+
return
|
|
1458
|
+
}
|
|
1459
|
+
if ((key.meta || key.option) && (key.name === "backspace" || key.name === "delete")) {
|
|
1460
|
+
editComment(key.name === "delete" ? deleteWordForward : deleteWordBackward)
|
|
1461
|
+
return
|
|
1462
|
+
}
|
|
1463
|
+
if (key.name === "backspace") {
|
|
1464
|
+
editComment(editorBackspace)
|
|
1465
|
+
return
|
|
1466
|
+
}
|
|
1467
|
+
if (key.name === "delete") {
|
|
1468
|
+
editComment(editorDeleteForward)
|
|
1469
|
+
return
|
|
1470
|
+
}
|
|
1471
|
+
if (key.name === "left") {
|
|
1472
|
+
editComment(editorMoveLeft)
|
|
1473
|
+
return
|
|
1474
|
+
}
|
|
1475
|
+
if (key.name === "right") {
|
|
1476
|
+
editComment(editorMoveRight)
|
|
1477
|
+
return
|
|
1478
|
+
}
|
|
1479
|
+
if (key.name === "up") {
|
|
1480
|
+
editComment((state) => moveVertically(state, -1))
|
|
1481
|
+
return
|
|
1482
|
+
}
|
|
1483
|
+
if (key.name === "down") {
|
|
1484
|
+
editComment((state) => moveVertically(state, 1))
|
|
1485
|
+
return
|
|
1486
|
+
}
|
|
1487
|
+
if (key.name === "home") {
|
|
1488
|
+
editComment(moveLineStart)
|
|
1489
|
+
return
|
|
1490
|
+
}
|
|
1491
|
+
if (key.name === "end") {
|
|
1492
|
+
editComment(moveLineEnd)
|
|
1493
|
+
return
|
|
1494
|
+
}
|
|
1495
|
+
if ((key.name === "return" || key.name === "enter") && key.shift) {
|
|
1496
|
+
editComment((state) => insertText(state, "\n"))
|
|
1497
|
+
return
|
|
1498
|
+
}
|
|
1499
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1500
|
+
submitDiffComment()
|
|
1501
|
+
return
|
|
1502
|
+
}
|
|
1503
|
+
if (!key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
1504
|
+
editComment((state) => insertText(state, key.sequence))
|
|
1505
|
+
return
|
|
1506
|
+
}
|
|
1507
|
+
return
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
if (commentThreadModalActive) {
|
|
1511
|
+
if (key.name === "escape") {
|
|
1512
|
+
closeActiveModal()
|
|
1513
|
+
return
|
|
1514
|
+
}
|
|
1515
|
+
if (key.name === "return" || key.name === "enter" || key.name === "a" || key.name === "c") {
|
|
1516
|
+
openDiffCommentModal()
|
|
1517
|
+
return
|
|
1518
|
+
}
|
|
1519
|
+
if (key.name === "up" || key.name === "k") {
|
|
1520
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - 1) }))
|
|
1521
|
+
return
|
|
1522
|
+
}
|
|
1523
|
+
if (key.name === "down" || key.name === "j") {
|
|
1524
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: current.scrollOffset + 1 }))
|
|
1525
|
+
return
|
|
1526
|
+
}
|
|
1527
|
+
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
1528
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - halfPage) }))
|
|
1529
|
+
return
|
|
1530
|
+
}
|
|
1531
|
+
if (key.name === "pagedown" || key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
1532
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: current.scrollOffset + halfPage }))
|
|
1533
|
+
return
|
|
1534
|
+
}
|
|
1535
|
+
return
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (closeModalActive) {
|
|
1539
|
+
if (key.name === "escape") {
|
|
1540
|
+
closeActiveModal()
|
|
1541
|
+
return
|
|
1542
|
+
}
|
|
1543
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1544
|
+
confirmClosePullRequest()
|
|
1545
|
+
return
|
|
1546
|
+
}
|
|
1547
|
+
return
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if (mergeModalActive) {
|
|
815
1551
|
const options = availableMergeActions(mergeModal.info)
|
|
816
1552
|
if (key.name === "escape") {
|
|
817
|
-
|
|
1553
|
+
closeActiveModal()
|
|
818
1554
|
return
|
|
819
1555
|
}
|
|
820
1556
|
if ((key.name === "return" || key.name === "enter") && options.length > 0) {
|
|
@@ -839,9 +1575,9 @@ export const App = () => {
|
|
|
839
1575
|
}
|
|
840
1576
|
|
|
841
1577
|
// Label modal takes priority over everything else
|
|
842
|
-
if (
|
|
1578
|
+
if (labelModalActive) {
|
|
843
1579
|
if (key.name === "escape") {
|
|
844
|
-
|
|
1580
|
+
closeActiveModal()
|
|
845
1581
|
return
|
|
846
1582
|
}
|
|
847
1583
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -889,28 +1625,94 @@ export const App = () => {
|
|
|
889
1625
|
}
|
|
890
1626
|
|
|
891
1627
|
if (diffFullView) {
|
|
1628
|
+
if (diffCommentMode) {
|
|
1629
|
+
if (key.name === "escape") {
|
|
1630
|
+
setDiffCommentMode(false)
|
|
1631
|
+
return
|
|
1632
|
+
}
|
|
1633
|
+
if (key.name === "c") {
|
|
1634
|
+
setDiffCommentMode(false)
|
|
1635
|
+
return
|
|
1636
|
+
}
|
|
1637
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1638
|
+
if (selectedDiffCommentThread.length > 0) openDiffCommentThreadModal()
|
|
1639
|
+
else openDiffCommentModal()
|
|
1640
|
+
return
|
|
1641
|
+
}
|
|
1642
|
+
if (key.name === "a") {
|
|
1643
|
+
openDiffCommentModal()
|
|
1644
|
+
return
|
|
1645
|
+
}
|
|
1646
|
+
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
1647
|
+
moveDiffCommentAnchor(-halfPage)
|
|
1648
|
+
return
|
|
1649
|
+
}
|
|
1650
|
+
if (key.name === "pagedown" || key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
1651
|
+
moveDiffCommentAnchor(halfPage)
|
|
1652
|
+
return
|
|
1653
|
+
}
|
|
1654
|
+
if ((key.shift || key.option || key.meta) && (key.name === "up" || key.name === "k") || key.name === "K") {
|
|
1655
|
+
moveDiffCommentAnchor(-8)
|
|
1656
|
+
return
|
|
1657
|
+
}
|
|
1658
|
+
if ((key.shift || key.option || key.meta) && (key.name === "down" || key.name === "j") || key.name === "J") {
|
|
1659
|
+
moveDiffCommentAnchor(8)
|
|
1660
|
+
return
|
|
1661
|
+
}
|
|
1662
|
+
if (key.name === "up" || key.name === "k") {
|
|
1663
|
+
moveDiffCommentAnchor(-1)
|
|
1664
|
+
return
|
|
1665
|
+
}
|
|
1666
|
+
if (key.name === "down" || key.name === "j") {
|
|
1667
|
+
moveDiffCommentAnchor(1)
|
|
1668
|
+
return
|
|
1669
|
+
}
|
|
1670
|
+
if (key.name === "left" || key.name === "h") {
|
|
1671
|
+
selectDiffCommentSide("LEFT")
|
|
1672
|
+
return
|
|
1673
|
+
}
|
|
1674
|
+
if (key.name === "right" || key.name === "l") {
|
|
1675
|
+
selectDiffCommentSide("RIGHT")
|
|
1676
|
+
return
|
|
1677
|
+
}
|
|
1678
|
+
if (key.name === "]" && selectedDiffState?.status === "ready") {
|
|
1679
|
+
jumpDiffFile(1)
|
|
1680
|
+
return
|
|
1681
|
+
}
|
|
1682
|
+
if (key.name === "[" && selectedDiffState?.status === "ready") {
|
|
1683
|
+
jumpDiffFile(-1)
|
|
1684
|
+
return
|
|
1685
|
+
}
|
|
1686
|
+
return
|
|
1687
|
+
}
|
|
1688
|
+
|
|
892
1689
|
if (key.name === "escape" || key.name === "return" || key.name === "enter") {
|
|
893
1690
|
setDiffFullView(false)
|
|
1691
|
+
setDiffCommentMode(false)
|
|
1692
|
+
return
|
|
1693
|
+
}
|
|
1694
|
+
if (key.name === "c" && selectedDiffState?.status === "ready") {
|
|
1695
|
+
enterDiffCommentMode()
|
|
894
1696
|
return
|
|
895
1697
|
}
|
|
896
1698
|
if (key.name === "home") {
|
|
897
|
-
|
|
1699
|
+
scrollDiffTo(0)
|
|
898
1700
|
return
|
|
899
1701
|
}
|
|
900
1702
|
if (key.name === "end") {
|
|
901
|
-
|
|
1703
|
+
scrollDiffTo(Number.MAX_SAFE_INTEGER)
|
|
902
1704
|
return
|
|
903
1705
|
}
|
|
904
1706
|
if (key.name === "pageup") {
|
|
905
|
-
|
|
1707
|
+
scrollDiffBy(-halfPage)
|
|
906
1708
|
return
|
|
907
1709
|
}
|
|
908
1710
|
if (key.name === "pagedown") {
|
|
909
|
-
|
|
1711
|
+
scrollDiffBy(halfPage)
|
|
910
1712
|
return
|
|
911
1713
|
}
|
|
912
1714
|
if (isShiftG(key)) {
|
|
913
|
-
|
|
1715
|
+
scrollDiffTo(Number.MAX_SAFE_INTEGER)
|
|
914
1716
|
setPendingG(false)
|
|
915
1717
|
if (pendingGTimeoutRef.current !== null) {
|
|
916
1718
|
clearTimeout(pendingGTimeoutRef.current)
|
|
@@ -920,7 +1722,7 @@ export const App = () => {
|
|
|
920
1722
|
}
|
|
921
1723
|
if (key.name === "g") {
|
|
922
1724
|
if (pendingG) {
|
|
923
|
-
|
|
1725
|
+
scrollDiffTo(0)
|
|
924
1726
|
setPendingG(false)
|
|
925
1727
|
if (pendingGTimeoutRef.current !== null) {
|
|
926
1728
|
clearTimeout(pendingGTimeoutRef.current)
|
|
@@ -936,19 +1738,19 @@ export const App = () => {
|
|
|
936
1738
|
return
|
|
937
1739
|
}
|
|
938
1740
|
if (key.name === "up" || key.name === "k") {
|
|
939
|
-
|
|
1741
|
+
scrollDiffBy(-1)
|
|
940
1742
|
return
|
|
941
1743
|
}
|
|
942
1744
|
if (key.name === "down" || key.name === "j") {
|
|
943
|
-
|
|
1745
|
+
scrollDiffBy(1)
|
|
944
1746
|
return
|
|
945
1747
|
}
|
|
946
1748
|
if (key.ctrl && key.name === "u") {
|
|
947
|
-
|
|
1749
|
+
scrollDiffBy(-halfPage)
|
|
948
1750
|
return
|
|
949
1751
|
}
|
|
950
1752
|
if (key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
951
|
-
|
|
1753
|
+
scrollDiffBy(halfPage)
|
|
952
1754
|
return
|
|
953
1755
|
}
|
|
954
1756
|
if (key.name === "v") {
|
|
@@ -960,18 +1762,16 @@ export const App = () => {
|
|
|
960
1762
|
return
|
|
961
1763
|
}
|
|
962
1764
|
if (key.name === "r" && selectedPullRequest) {
|
|
963
|
-
loadPullRequestDiff(selectedPullRequest, true)
|
|
1765
|
+
loadPullRequestDiff(selectedPullRequest, { force: true, includeComments: true })
|
|
964
1766
|
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
965
1767
|
return
|
|
966
1768
|
}
|
|
967
1769
|
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?.status === "ready") {
|
|
968
|
-
|
|
969
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1770
|
+
jumpDiffFile(1)
|
|
970
1771
|
return
|
|
971
1772
|
}
|
|
972
1773
|
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?.status === "ready") {
|
|
973
|
-
|
|
974
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1774
|
+
jumpDiffFile(-1)
|
|
975
1775
|
return
|
|
976
1776
|
}
|
|
977
1777
|
if (key.name === "o" && selectedPullRequest) {
|
|
@@ -983,11 +1783,40 @@ export const App = () => {
|
|
|
983
1783
|
|
|
984
1784
|
// Fullscreen detail mode handles its own navigation keys.
|
|
985
1785
|
if (detailFullView) {
|
|
1786
|
+
const plainKey = !key.ctrl && !key.meta && !key.option
|
|
986
1787
|
if (key.name === "escape" || (key.name === "return" || key.name === "enter")) {
|
|
987
1788
|
setDetailFullView(false)
|
|
988
1789
|
setDetailScrollOffset(0)
|
|
989
1790
|
return
|
|
990
1791
|
}
|
|
1792
|
+
if (isThemeKey(key)) {
|
|
1793
|
+
openThemeModal()
|
|
1794
|
+
return
|
|
1795
|
+
}
|
|
1796
|
+
if (plainKey && key.name === "d" && selectedPullRequest) {
|
|
1797
|
+
openDiffView()
|
|
1798
|
+
return
|
|
1799
|
+
}
|
|
1800
|
+
if (plainKey && key.name === "x" && selectedPullRequest?.state === "open") {
|
|
1801
|
+
openCloseModal()
|
|
1802
|
+
return
|
|
1803
|
+
}
|
|
1804
|
+
if (plainKey && key.name === "l" && selectedPullRequest) {
|
|
1805
|
+
openLabelModal()
|
|
1806
|
+
return
|
|
1807
|
+
}
|
|
1808
|
+
if (plainKey && (key.name === "m" || key.name === "M") && selectedPullRequest) {
|
|
1809
|
+
openMergeModal()
|
|
1810
|
+
return
|
|
1811
|
+
}
|
|
1812
|
+
if (plainKey && (key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
1813
|
+
toggleSelectedPullRequestDraftStatus()
|
|
1814
|
+
return
|
|
1815
|
+
}
|
|
1816
|
+
if (plainKey && key.name === "r") {
|
|
1817
|
+
refreshPullRequests("Refreshed")
|
|
1818
|
+
return
|
|
1819
|
+
}
|
|
991
1820
|
if (key.name === "home") {
|
|
992
1821
|
detailScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
993
1822
|
setDetailScrollOffset(0)
|
|
@@ -1051,14 +1880,12 @@ export const App = () => {
|
|
|
1051
1880
|
setDetailScrollOffset((current) => current + halfPage)
|
|
1052
1881
|
return
|
|
1053
1882
|
}
|
|
1054
|
-
if (key.name === "o" && selectedPullRequest) {
|
|
1883
|
+
if (plainKey && key.name === "o" && selectedPullRequest) {
|
|
1055
1884
|
openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
1056
1885
|
return
|
|
1057
1886
|
}
|
|
1058
|
-
if (key.name === "y" && selectedPullRequest) {
|
|
1059
|
-
|
|
1060
|
-
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1061
|
-
.catch((error) => flashNotice(error instanceof Error ? error.message : String(error)))
|
|
1887
|
+
if (plainKey && key.name === "y" && selectedPullRequest) {
|
|
1888
|
+
copySelectedPullRequestMetadata()
|
|
1062
1889
|
return
|
|
1063
1890
|
}
|
|
1064
1891
|
return
|
|
@@ -1093,6 +1920,11 @@ export const App = () => {
|
|
|
1093
1920
|
}
|
|
1094
1921
|
}
|
|
1095
1922
|
|
|
1923
|
+
if (key.name === "tab") {
|
|
1924
|
+
switchQueueMode(key.shift ? -1 : 1)
|
|
1925
|
+
return
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1096
1928
|
if (isThemeKey(key)) {
|
|
1097
1929
|
openThemeModal()
|
|
1098
1930
|
return
|
|
@@ -1110,7 +1942,7 @@ export const App = () => {
|
|
|
1110
1942
|
return
|
|
1111
1943
|
}
|
|
1112
1944
|
if (key.name === "r") {
|
|
1113
|
-
refreshPullRequests("
|
|
1945
|
+
refreshPullRequests("Refreshed")
|
|
1114
1946
|
return
|
|
1115
1947
|
}
|
|
1116
1948
|
if (
|
|
@@ -1199,10 +2031,14 @@ export const App = () => {
|
|
|
1199
2031
|
setDetailScrollOffset(0)
|
|
1200
2032
|
return
|
|
1201
2033
|
}
|
|
1202
|
-
if (
|
|
2034
|
+
if (key.name === "d" && selectedPullRequest) {
|
|
1203
2035
|
openDiffView()
|
|
1204
2036
|
return
|
|
1205
2037
|
}
|
|
2038
|
+
if (key.name === "x" && selectedPullRequest?.state === "open") {
|
|
2039
|
+
openCloseModal()
|
|
2040
|
+
return
|
|
2041
|
+
}
|
|
1206
2042
|
if (key.name === "l" && selectedPullRequest) {
|
|
1207
2043
|
openLabelModal()
|
|
1208
2044
|
return
|
|
@@ -1216,30 +2052,12 @@ export const App = () => {
|
|
|
1216
2052
|
return
|
|
1217
2053
|
}
|
|
1218
2054
|
if ((key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
1219
|
-
|
|
1220
|
-
const nextReviewStatus = selectedPullRequest.reviewStatus === "draft" ? "review" : "draft"
|
|
1221
|
-
updatePullRequest(selectedPullRequest.url, (pullRequest) => ({
|
|
1222
|
-
...pullRequest,
|
|
1223
|
-
reviewStatus: nextReviewStatus,
|
|
1224
|
-
}))
|
|
1225
|
-
void toggleDraftStatus({ repository: selectedPullRequest.repository, number: selectedPullRequest.number, isDraft: selectedPullRequest.reviewStatus === "draft" })
|
|
1226
|
-
.then(() => {
|
|
1227
|
-
flashNotice(selectedPullRequest.reviewStatus === "draft" ? `Marked #${selectedPullRequest.number} ready` : `Marked #${selectedPullRequest.number} draft`)
|
|
1228
|
-
})
|
|
1229
|
-
.catch((error) => {
|
|
1230
|
-
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
1231
|
-
flashNotice(error instanceof Error ? error.message : String(error))
|
|
1232
|
-
})
|
|
2055
|
+
toggleSelectedPullRequestDraftStatus()
|
|
1233
2056
|
return
|
|
1234
2057
|
}
|
|
1235
2058
|
if (key.name === "y" && selectedPullRequest) {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
flashNotice(`Copied #${selectedPullRequest.number} metadata`)
|
|
1239
|
-
})
|
|
1240
|
-
.catch((error) => {
|
|
1241
|
-
flashNotice(error instanceof Error ? error.message : String(error))
|
|
1242
|
-
})
|
|
2059
|
+
copySelectedPullRequestMetadata()
|
|
2060
|
+
return
|
|
1243
2061
|
}
|
|
1244
2062
|
})
|
|
1245
2063
|
|
|
@@ -1276,16 +2094,31 @@ export const App = () => {
|
|
|
1276
2094
|
const longestLabelName = labelModal.availableLabels.reduce((max, label) => Math.max(max, label.name.length), 0)
|
|
1277
2095
|
const labelModalWidth = Math.min(Math.max(42, longestLabelName + 16), 56, contentWidth - 4)
|
|
1278
2096
|
const labelModalHeight = Math.min(20, terminalHeight - 4)
|
|
1279
|
-
const labelModalLeft =
|
|
1280
|
-
const labelModalTop =
|
|
2097
|
+
const labelModalLeft = centeredOffset(contentWidth, labelModalWidth)
|
|
2098
|
+
const labelModalTop = centeredOffset(terminalHeight, labelModalHeight)
|
|
2099
|
+
const closeModalWidth = Math.min(68, Math.max(46, contentWidth - 12))
|
|
2100
|
+
const closeModalHeight = Math.min(12, terminalHeight - 4)
|
|
2101
|
+
const closeModalLeft = centeredOffset(contentWidth, closeModalWidth)
|
|
2102
|
+
const closeModalTop = centeredOffset(terminalHeight, closeModalHeight)
|
|
2103
|
+
const commentModalWidth = Math.min(76, Math.max(46, contentWidth - 8))
|
|
2104
|
+
const commentModalHeight = Math.min(16, terminalHeight - 4)
|
|
2105
|
+
const commentModalLeft = centeredOffset(contentWidth, commentModalWidth)
|
|
2106
|
+
const commentModalTop = centeredOffset(terminalHeight, commentModalHeight)
|
|
2107
|
+
const commentThreadModalWidth = Math.min(86, Math.max(50, contentWidth - 8))
|
|
2108
|
+
const commentThreadModalHeight = Math.min(22, terminalHeight - 4)
|
|
2109
|
+
const commentThreadModalLeft = centeredOffset(contentWidth, commentThreadModalWidth)
|
|
2110
|
+
const commentThreadModalTop = centeredOffset(terminalHeight, commentThreadModalHeight)
|
|
2111
|
+
const commentAnchorLabel = selectedDiffCommentAnchor
|
|
2112
|
+
? `${selectedDiffCommentAnchor.path}:${selectedDiffCommentAnchor.line} ${selectedDiffCommentAnchor.side === "RIGHT" ? "right" : "left"}`
|
|
2113
|
+
: "No diff line selected"
|
|
1281
2114
|
const mergeModalWidth = Math.min(68, Math.max(46, contentWidth - 12))
|
|
1282
2115
|
const mergeModalHeight = Math.min(16, terminalHeight - 4)
|
|
1283
|
-
const mergeModalLeft =
|
|
1284
|
-
const mergeModalTop =
|
|
2116
|
+
const mergeModalLeft = centeredOffset(contentWidth, mergeModalWidth)
|
|
2117
|
+
const mergeModalTop = centeredOffset(terminalHeight, mergeModalHeight)
|
|
1285
2118
|
const themeModalWidth = Math.min(58, Math.max(38, contentWidth - 12))
|
|
1286
2119
|
const themeModalHeight = Math.min(16, terminalHeight - 4)
|
|
1287
|
-
const themeModalLeft =
|
|
1288
|
-
const themeModalTop =
|
|
2120
|
+
const themeModalLeft = centeredOffset(contentWidth, themeModalWidth)
|
|
2121
|
+
const themeModalTop = centeredOffset(terminalHeight, themeModalHeight)
|
|
1289
2122
|
|
|
1290
2123
|
return (
|
|
1291
2124
|
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
@@ -1303,6 +2136,7 @@ export const App = () => {
|
|
|
1303
2136
|
<PullRequestDiffPane
|
|
1304
2137
|
pullRequest={selectedPullRequest}
|
|
1305
2138
|
diffState={selectedDiffState}
|
|
2139
|
+
stackedFiles={stackedDiffFiles}
|
|
1306
2140
|
fileIndex={diffFileIndex}
|
|
1307
2141
|
view={effectiveDiffRenderView}
|
|
1308
2142
|
wrapMode={diffWrapMode}
|
|
@@ -1310,6 +2144,11 @@ export const App = () => {
|
|
|
1310
2144
|
height={wideBodyHeight}
|
|
1311
2145
|
loadingIndicator={loadingIndicator}
|
|
1312
2146
|
scrollRef={diffScrollRef}
|
|
2147
|
+
setDiffRef={setDiffRenderableRef}
|
|
2148
|
+
commentMode={diffCommentMode}
|
|
2149
|
+
selectedCommentAnchor={selectedDiffCommentAnchor}
|
|
2150
|
+
selectedCommentThread={selectedDiffCommentThread}
|
|
2151
|
+
commentCount={selectedDiffCommentCount}
|
|
1313
2152
|
themeId={themeId}
|
|
1314
2153
|
/>
|
|
1315
2154
|
) : isWideLayout && detailFullView ? (
|
|
@@ -1317,6 +2156,7 @@ export const App = () => {
|
|
|
1317
2156
|
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: wideFullscreenDetailScrollable }}>
|
|
1318
2157
|
<DetailsPane
|
|
1319
2158
|
pullRequest={selectedPullRequest}
|
|
2159
|
+
viewerUsername={username}
|
|
1320
2160
|
contentWidth={fullscreenContentWidth}
|
|
1321
2161
|
bodyLines={fullscreenBodyLines}
|
|
1322
2162
|
paneWidth={contentWidth}
|
|
@@ -1338,7 +2178,7 @@ export const App = () => {
|
|
|
1338
2178
|
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
1339
2179
|
{selectedPullRequest ? (
|
|
1340
2180
|
<>
|
|
1341
|
-
<DetailHeader pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2181
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
1342
2182
|
<scrollbox flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
1343
2183
|
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
1344
2184
|
</scrollbox>
|
|
@@ -1353,6 +2193,7 @@ export const App = () => {
|
|
|
1353
2193
|
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: narrowFullscreenDetailScrollable }}>
|
|
1354
2194
|
<DetailsPane
|
|
1355
2195
|
pullRequest={selectedPullRequest}
|
|
2196
|
+
viewerUsername={username}
|
|
1356
2197
|
contentWidth={fullscreenContentWidth}
|
|
1357
2198
|
bodyLines={fullscreenBodyLines}
|
|
1358
2199
|
paneWidth={contentWidth}
|
|
@@ -1364,7 +2205,7 @@ export const App = () => {
|
|
|
1364
2205
|
</box>
|
|
1365
2206
|
) : (
|
|
1366
2207
|
<box height={wideBodyHeight} flexDirection="column">
|
|
1367
|
-
<DetailsPane pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2208
|
+
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
1368
2209
|
<Divider width={contentWidth} />
|
|
1369
2210
|
<box flexGrow={1} flexDirection="column">
|
|
1370
2211
|
<scrollbox flexGrow={1}>
|
|
@@ -1390,15 +2231,17 @@ export const App = () => {
|
|
|
1390
2231
|
showFilterClear={filterMode || filterQuery.length > 0}
|
|
1391
2232
|
detailFullView={detailFullView}
|
|
1392
2233
|
diffFullView={diffFullView}
|
|
2234
|
+
diffCommentMode={diffCommentMode}
|
|
1393
2235
|
hasSelection={selectedPullRequest !== null}
|
|
2236
|
+
canCloseSelection={selectedPullRequest?.state === "open"}
|
|
1394
2237
|
hasError={pullRequestStatus === "error"}
|
|
1395
|
-
isLoading={pullRequestStatus === "loading"}
|
|
2238
|
+
isLoading={pullRequestStatus === "loading" || isRefreshingPullRequests || isHydratingPullRequestDetails || closeModal.running || mergeModal.running}
|
|
1396
2239
|
loadingIndicator={loadingIndicator}
|
|
1397
2240
|
retryProgress={retryProgress}
|
|
1398
2241
|
/>
|
|
1399
2242
|
)}
|
|
1400
2243
|
</box>
|
|
1401
|
-
{
|
|
2244
|
+
{labelModalActive ? (
|
|
1402
2245
|
<LabelModal
|
|
1403
2246
|
state={labelModal}
|
|
1404
2247
|
currentLabels={selectedPullRequest?.labels ?? []}
|
|
@@ -1409,7 +2252,38 @@ export const App = () => {
|
|
|
1409
2252
|
loadingIndicator={loadingIndicator}
|
|
1410
2253
|
/>
|
|
1411
2254
|
) : null}
|
|
1412
|
-
{
|
|
2255
|
+
{closeModalActive ? (
|
|
2256
|
+
<CloseModal
|
|
2257
|
+
state={closeModal}
|
|
2258
|
+
modalWidth={closeModalWidth}
|
|
2259
|
+
modalHeight={closeModalHeight}
|
|
2260
|
+
offsetLeft={closeModalLeft}
|
|
2261
|
+
offsetTop={closeModalTop}
|
|
2262
|
+
loadingIndicator={loadingIndicator}
|
|
2263
|
+
/>
|
|
2264
|
+
) : null}
|
|
2265
|
+
{commentModalActive ? (
|
|
2266
|
+
<CommentModal
|
|
2267
|
+
state={commentModal}
|
|
2268
|
+
anchorLabel={commentAnchorLabel}
|
|
2269
|
+
modalWidth={commentModalWidth}
|
|
2270
|
+
modalHeight={commentModalHeight}
|
|
2271
|
+
offsetLeft={commentModalLeft}
|
|
2272
|
+
offsetTop={commentModalTop}
|
|
2273
|
+
/>
|
|
2274
|
+
) : null}
|
|
2275
|
+
{commentThreadModalActive ? (
|
|
2276
|
+
<CommentThreadModal
|
|
2277
|
+
state={commentThreadModal}
|
|
2278
|
+
anchorLabel={commentAnchorLabel}
|
|
2279
|
+
comments={selectedDiffCommentThread}
|
|
2280
|
+
modalWidth={commentThreadModalWidth}
|
|
2281
|
+
modalHeight={commentThreadModalHeight}
|
|
2282
|
+
offsetLeft={commentThreadModalLeft}
|
|
2283
|
+
offsetTop={commentThreadModalTop}
|
|
2284
|
+
/>
|
|
2285
|
+
) : null}
|
|
2286
|
+
{mergeModalActive ? (
|
|
1413
2287
|
<MergeModal
|
|
1414
2288
|
state={mergeModal}
|
|
1415
2289
|
modalWidth={mergeModalWidth}
|
|
@@ -1419,7 +2293,7 @@ export const App = () => {
|
|
|
1419
2293
|
loadingIndicator={loadingIndicator}
|
|
1420
2294
|
/>
|
|
1421
2295
|
) : null}
|
|
1422
|
-
{
|
|
2296
|
+
{themeModalActive ? (
|
|
1423
2297
|
<ThemeModal
|
|
1424
2298
|
state={themeModal}
|
|
1425
2299
|
activeThemeId={themeId}
|