@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/ui/modals.tsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { TextAttributes } from "@opentui/core"
|
|
2
|
-
import
|
|
2
|
+
import { Data } from "effect"
|
|
3
|
+
import { formatShortDate, formatTimestamp } from "../date.js"
|
|
4
|
+
import type { PullRequestLabel, PullRequestMergeInfo, PullRequestReviewComment } from "../domain.js"
|
|
3
5
|
import { availableMergeActions } from "../mergeActions.js"
|
|
6
|
+
import { clampCursor, commentEditorLines, cursorLineIndexForLines } from "./commentEditor.js"
|
|
4
7
|
import { colors, filterThemeDefinitions, themeDefinitions, type ThemeId } from "./colors.js"
|
|
5
8
|
import { centerCell, Divider, fitCell, ModalFrame, PlainLine, TextLine } from "./primitives.js"
|
|
6
9
|
import { labelColor, shortRepoName } from "./pullRequests.js"
|
|
7
10
|
|
|
8
11
|
export interface LabelModalState {
|
|
9
|
-
readonly open: boolean
|
|
10
12
|
readonly repository: string | null
|
|
11
13
|
readonly query: string
|
|
12
14
|
readonly selectedIndex: number
|
|
@@ -15,7 +17,6 @@ export interface LabelModalState {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface MergeModalState {
|
|
18
|
-
readonly open: boolean
|
|
19
20
|
readonly repository: string | null
|
|
20
21
|
readonly number: number | null
|
|
21
22
|
readonly selectedIndex: number
|
|
@@ -25,15 +26,32 @@ export interface MergeModalState {
|
|
|
25
26
|
readonly error: string | null
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
export interface CloseModalState {
|
|
30
|
+
readonly repository: string | null
|
|
31
|
+
readonly number: number | null
|
|
32
|
+
readonly title: string
|
|
33
|
+
readonly url: string | null
|
|
34
|
+
readonly running: boolean
|
|
35
|
+
readonly error: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CommentModalState {
|
|
39
|
+
readonly body: string
|
|
40
|
+
readonly cursor: number
|
|
41
|
+
readonly error: string | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CommentThreadModalState {
|
|
45
|
+
readonly scrollOffset: number
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
export interface ThemeModalState {
|
|
29
|
-
readonly open: boolean
|
|
30
49
|
readonly query: string
|
|
31
50
|
readonly filterMode: boolean
|
|
32
51
|
readonly initialThemeId: ThemeId
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
export const initialLabelModalState: LabelModalState = {
|
|
36
|
-
open: false,
|
|
37
55
|
repository: null,
|
|
38
56
|
query: "",
|
|
39
57
|
selectedIndex: 0,
|
|
@@ -42,7 +60,6 @@ export const initialLabelModalState: LabelModalState = {
|
|
|
42
60
|
}
|
|
43
61
|
|
|
44
62
|
export const initialMergeModalState: MergeModalState = {
|
|
45
|
-
open: false,
|
|
46
63
|
repository: null,
|
|
47
64
|
number: null,
|
|
48
65
|
selectedIndex: 0,
|
|
@@ -52,13 +69,56 @@ export const initialMergeModalState: MergeModalState = {
|
|
|
52
69
|
error: null,
|
|
53
70
|
}
|
|
54
71
|
|
|
72
|
+
export const initialCloseModalState: CloseModalState = {
|
|
73
|
+
repository: null,
|
|
74
|
+
number: null,
|
|
75
|
+
title: "",
|
|
76
|
+
url: null,
|
|
77
|
+
running: false,
|
|
78
|
+
error: null,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const initialCommentModalState: CommentModalState = {
|
|
82
|
+
body: "",
|
|
83
|
+
cursor: 0,
|
|
84
|
+
error: null,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const initialCommentThreadModalState: CommentThreadModalState = {
|
|
88
|
+
scrollOffset: 0,
|
|
89
|
+
}
|
|
90
|
+
|
|
55
91
|
export const initialThemeModalState: ThemeModalState = {
|
|
56
|
-
open: false,
|
|
57
92
|
query: "",
|
|
58
93
|
filterMode: false,
|
|
59
94
|
initialThemeId: "ghui",
|
|
60
95
|
}
|
|
61
96
|
|
|
97
|
+
export type Modal = Data.TaggedEnum<{
|
|
98
|
+
None: {}
|
|
99
|
+
Label: { readonly state: LabelModalState }
|
|
100
|
+
Close: { readonly state: CloseModalState }
|
|
101
|
+
Merge: { readonly state: MergeModalState }
|
|
102
|
+
Comment: { readonly state: CommentModalState }
|
|
103
|
+
CommentThread: { readonly state: CommentThreadModalState }
|
|
104
|
+
Theme: { readonly state: ThemeModalState }
|
|
105
|
+
}>
|
|
106
|
+
|
|
107
|
+
export const Modal = Data.taggedEnum<Modal>()
|
|
108
|
+
export const initialModal: Modal = Modal.None()
|
|
109
|
+
|
|
110
|
+
export type ModalTag = Modal["_tag"]
|
|
111
|
+
export type ModalState<Tag extends Exclude<ModalTag, "None">> = Extract<Modal, { _tag: Tag }>["state"]
|
|
112
|
+
|
|
113
|
+
export const modalInitialStates = {
|
|
114
|
+
Label: initialLabelModalState,
|
|
115
|
+
Close: initialCloseModalState,
|
|
116
|
+
Merge: initialMergeModalState,
|
|
117
|
+
Comment: initialCommentModalState,
|
|
118
|
+
CommentThread: initialCommentThreadModalState,
|
|
119
|
+
Theme: initialThemeModalState,
|
|
120
|
+
} as const satisfies { [Tag in Exclude<ModalTag, "None">]: ModalState<Tag> }
|
|
121
|
+
|
|
62
122
|
const mergeUnavailableReason = (info: PullRequestMergeInfo | null) => {
|
|
63
123
|
if (!info) return "Loading merge status from GitHub."
|
|
64
124
|
if (info.state !== "open") return "This pull request is not open."
|
|
@@ -191,7 +251,7 @@ export const MergeModal = ({
|
|
|
191
251
|
const options = availableMergeActions(state.info)
|
|
192
252
|
const selectedIndex = options.length === 0 ? 0 : Math.max(0, Math.min(state.selectedIndex, options.length - 1))
|
|
193
253
|
const title = state.info ? `Merge #${state.info.number}` : state.number ? `Merge #${state.number}` : "Merge"
|
|
194
|
-
const rightText = state.running ?
|
|
254
|
+
const rightText = state.running ? `${loadingIndicator} running` : state.loading ? `${loadingIndicator} loading` : state.info?.autoMergeEnabled ? "auto on" : "manual"
|
|
195
255
|
const headerGap = Math.max(1, contentWidth - title.length - rightText.length)
|
|
196
256
|
const repo = state.info?.repository ?? state.repository
|
|
197
257
|
const statusLine = state.info
|
|
@@ -262,6 +322,260 @@ export const MergeModal = ({
|
|
|
262
322
|
)
|
|
263
323
|
}
|
|
264
324
|
|
|
325
|
+
export const CloseModal = ({
|
|
326
|
+
state,
|
|
327
|
+
modalWidth,
|
|
328
|
+
modalHeight,
|
|
329
|
+
offsetLeft,
|
|
330
|
+
offsetTop,
|
|
331
|
+
loadingIndicator,
|
|
332
|
+
}: {
|
|
333
|
+
state: CloseModalState
|
|
334
|
+
modalWidth: number
|
|
335
|
+
modalHeight: number
|
|
336
|
+
offsetLeft: number
|
|
337
|
+
offsetTop: number
|
|
338
|
+
loadingIndicator: string
|
|
339
|
+
}) => {
|
|
340
|
+
const innerWidth = Math.max(16, modalWidth - 2)
|
|
341
|
+
const contentWidth = Math.max(14, innerWidth - 2)
|
|
342
|
+
const title = state.number ? `Close #${state.number}` : "Close pull request"
|
|
343
|
+
const rightText = state.running ? `${loadingIndicator} closing` : "confirm"
|
|
344
|
+
const headerGap = Math.max(1, contentWidth - title.length - rightText.length)
|
|
345
|
+
const repo = state.repository ? shortRepoName(state.repository) : ""
|
|
346
|
+
const titleLines = [
|
|
347
|
+
fitCell(repo, contentWidth),
|
|
348
|
+
fitCell(state.title, contentWidth),
|
|
349
|
+
]
|
|
350
|
+
const bodyHeight = Math.max(1, modalHeight - 7)
|
|
351
|
+
const topRows = Math.max(0, Math.floor((bodyHeight - titleLines.length - 2) / 2))
|
|
352
|
+
const bottomRows = Math.max(0, bodyHeight - topRows - titleLines.length - 2)
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[2, modalHeight - 4]}>
|
|
356
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
357
|
+
<TextLine>
|
|
358
|
+
<span fg={colors.error} attributes={TextAttributes.BOLD}>{title}</span>
|
|
359
|
+
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
360
|
+
<span fg={state.running ? colors.status.pending : colors.muted}>{rightText}</span>
|
|
361
|
+
</TextLine>
|
|
362
|
+
</box>
|
|
363
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
364
|
+
<PlainLine text={fitCell("This will close the pull request without merging it.", contentWidth)} fg={colors.muted} />
|
|
365
|
+
</box>
|
|
366
|
+
<Divider width={innerWidth} />
|
|
367
|
+
<box height={bodyHeight} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
368
|
+
{state.error ? (
|
|
369
|
+
<PlainLine text={fitCell(state.error, contentWidth)} fg={colors.error} />
|
|
370
|
+
) : (
|
|
371
|
+
<>
|
|
372
|
+
{Array.from({ length: topRows }, (_, index) => <box key={`top-${index}`} height={1} />)}
|
|
373
|
+
<PlainLine text={titleLines[0]!} fg={colors.muted} />
|
|
374
|
+
<PlainLine text={titleLines[1]!} fg={colors.text} bold />
|
|
375
|
+
{Array.from({ length: bottomRows }, (_, index) => <box key={`bottom-${index}`} height={1} />)}
|
|
376
|
+
</>
|
|
377
|
+
)}
|
|
378
|
+
</box>
|
|
379
|
+
<Divider width={innerWidth} />
|
|
380
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
381
|
+
<TextLine>
|
|
382
|
+
<span fg={colors.count}>enter</span>
|
|
383
|
+
<span fg={colors.muted}> close </span>
|
|
384
|
+
<span fg={colors.count}>esc</span>
|
|
385
|
+
<span fg={colors.muted}> cancel</span>
|
|
386
|
+
</TextLine>
|
|
387
|
+
</box>
|
|
388
|
+
</ModalFrame>
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export const CommentModal = ({
|
|
393
|
+
state,
|
|
394
|
+
anchorLabel,
|
|
395
|
+
modalWidth,
|
|
396
|
+
modalHeight,
|
|
397
|
+
offsetLeft,
|
|
398
|
+
offsetTop,
|
|
399
|
+
}: {
|
|
400
|
+
state: CommentModalState
|
|
401
|
+
anchorLabel: string
|
|
402
|
+
modalWidth: number
|
|
403
|
+
modalHeight: number
|
|
404
|
+
offsetLeft: number
|
|
405
|
+
offsetTop: number
|
|
406
|
+
}) => {
|
|
407
|
+
const innerWidth = Math.max(16, modalWidth - 2)
|
|
408
|
+
const contentWidth = Math.max(14, innerWidth - 2)
|
|
409
|
+
const title = "Comment"
|
|
410
|
+
const rightText = "enter save"
|
|
411
|
+
const headerGap = Math.max(1, contentWidth - title.length - rightText.length)
|
|
412
|
+
const bodyHeight = Math.max(1, modalHeight - 7)
|
|
413
|
+
const editorHeight = Math.max(1, bodyHeight - (state.error ? 1 : 0))
|
|
414
|
+
const lineRanges = commentEditorLines(state.body)
|
|
415
|
+
const cursor = clampCursor(state.body, state.cursor)
|
|
416
|
+
const cursorLineIndex = cursorLineIndexForLines(lineRanges, cursor)
|
|
417
|
+
const visibleStart = Math.min(
|
|
418
|
+
Math.max(0, lineRanges.length - editorHeight),
|
|
419
|
+
Math.max(0, cursorLineIndex - editorHeight + 1),
|
|
420
|
+
)
|
|
421
|
+
const visibleLines = lineRanges.slice(visibleStart, visibleStart + editorHeight)
|
|
422
|
+
const renderEditorLine = (line: { readonly text: string; readonly start: number; readonly end: number }, index: number) => {
|
|
423
|
+
const lineIndex = visibleStart + index
|
|
424
|
+
const isCursorLine = lineIndex === cursorLineIndex
|
|
425
|
+
const cursorColumn = Math.max(0, Math.min(cursor - line.start, line.text.length))
|
|
426
|
+
const viewStart = isCursorLine ? Math.max(0, cursorColumn - contentWidth + 1) : 0
|
|
427
|
+
const visibleText = line.text.slice(viewStart, viewStart + contentWidth)
|
|
428
|
+
|
|
429
|
+
if (!isCursorLine) {
|
|
430
|
+
return <PlainLine key={lineIndex} text={fitCell(visibleText, contentWidth)} fg={state.body.length > 0 ? colors.text : colors.muted} />
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const cursorInView = cursorColumn - viewStart
|
|
434
|
+
const before = visibleText.slice(0, cursorInView)
|
|
435
|
+
const placeholder = state.body.length === 0 ? "Write a comment..." : ""
|
|
436
|
+
const cursorChar = placeholder ? placeholder[0] ?? " " : visibleText[cursorInView] ?? " "
|
|
437
|
+
const after = placeholder ? placeholder.slice(1) : visibleText.slice(cursorInView + 1)
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<TextLine key={lineIndex}>
|
|
441
|
+
{before ? <span fg={colors.text}>{before}</span> : null}
|
|
442
|
+
<span bg={colors.accent} fg={colors.background}>{cursorChar}</span>
|
|
443
|
+
{after ? <span fg={placeholder ? colors.muted : colors.text}>{after}</span> : null}
|
|
444
|
+
</TextLine>
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[2, modalHeight - 4]}>
|
|
450
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
451
|
+
<TextLine>
|
|
452
|
+
<span fg={colors.accent} attributes={TextAttributes.BOLD}>{title}</span>
|
|
453
|
+
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
454
|
+
<span fg={colors.muted}>{rightText}</span>
|
|
455
|
+
</TextLine>
|
|
456
|
+
</box>
|
|
457
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
458
|
+
<PlainLine text={fitCell(anchorLabel, contentWidth)} fg={colors.muted} />
|
|
459
|
+
</box>
|
|
460
|
+
<Divider width={innerWidth} />
|
|
461
|
+
<box height={bodyHeight} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
462
|
+
{state.error ? <PlainLine text={fitCell(state.error, contentWidth)} fg={colors.error} /> : null}
|
|
463
|
+
{visibleLines.map(renderEditorLine)}
|
|
464
|
+
</box>
|
|
465
|
+
<Divider width={innerWidth} />
|
|
466
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
467
|
+
<TextLine>
|
|
468
|
+
<span fg={colors.count}>enter</span>
|
|
469
|
+
<span fg={colors.muted}> save </span>
|
|
470
|
+
<span fg={colors.count}>shift-enter</span>
|
|
471
|
+
<span fg={colors.muted}> newline </span>
|
|
472
|
+
<span fg={colors.count}>esc</span>
|
|
473
|
+
<span fg={colors.muted}> cancel</span>
|
|
474
|
+
</TextLine>
|
|
475
|
+
</box>
|
|
476
|
+
</ModalFrame>
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
type CommentThreadRow = {
|
|
481
|
+
readonly key: string
|
|
482
|
+
readonly text: string
|
|
483
|
+
readonly fg: string
|
|
484
|
+
readonly bold?: boolean
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const wrapCommentBody = (body: string, width: number) => {
|
|
488
|
+
const lines = body.length === 0 ? [""] : body.split("\n")
|
|
489
|
+
return lines.flatMap((line) => {
|
|
490
|
+
if (line.length === 0) return [""]
|
|
491
|
+
const wrapped: string[] = []
|
|
492
|
+
for (let index = 0; index < line.length; index += width) {
|
|
493
|
+
wrapped.push(line.slice(index, index + width))
|
|
494
|
+
}
|
|
495
|
+
return wrapped
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const formatCommentDate = (date: Date | null) => date ? `${formatShortDate(date)} ${formatTimestamp(date)}` : ""
|
|
500
|
+
|
|
501
|
+
const commentThreadRows = (comments: readonly PullRequestReviewComment[], width: number): readonly CommentThreadRow[] =>
|
|
502
|
+
comments.flatMap((comment, commentIndex) => {
|
|
503
|
+
const timestamp = formatCommentDate(comment.createdAt)
|
|
504
|
+
const header = timestamp ? `${comment.author} ${timestamp}` : comment.author
|
|
505
|
+
return [
|
|
506
|
+
{ key: `${comment.id}:header`, text: header, fg: colors.count, bold: true },
|
|
507
|
+
...wrapCommentBody(comment.body, width).map((line, lineIndex) => ({
|
|
508
|
+
key: `${comment.id}:body:${lineIndex}`,
|
|
509
|
+
text: line,
|
|
510
|
+
fg: colors.text,
|
|
511
|
+
})),
|
|
512
|
+
...(commentIndex < comments.length - 1 ? [{ key: `${comment.id}:gap`, text: "", fg: colors.muted }] : []),
|
|
513
|
+
]
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
export const CommentThreadModal = ({
|
|
517
|
+
state,
|
|
518
|
+
anchorLabel,
|
|
519
|
+
comments,
|
|
520
|
+
modalWidth,
|
|
521
|
+
modalHeight,
|
|
522
|
+
offsetLeft,
|
|
523
|
+
offsetTop,
|
|
524
|
+
}: {
|
|
525
|
+
state: CommentThreadModalState
|
|
526
|
+
anchorLabel: string
|
|
527
|
+
comments: readonly PullRequestReviewComment[]
|
|
528
|
+
modalWidth: number
|
|
529
|
+
modalHeight: number
|
|
530
|
+
offsetLeft: number
|
|
531
|
+
offsetTop: number
|
|
532
|
+
}) => {
|
|
533
|
+
const innerWidth = Math.max(16, modalWidth - 2)
|
|
534
|
+
const contentWidth = Math.max(14, innerWidth - 2)
|
|
535
|
+
const title = "Thread"
|
|
536
|
+
const countText = comments.length === 1 ? "1 comment" : `${comments.length} comments`
|
|
537
|
+
const headerGap = Math.max(1, contentWidth - title.length - countText.length)
|
|
538
|
+
const bodyHeight = Math.max(1, modalHeight - 7)
|
|
539
|
+
const rows = commentThreadRows(comments, contentWidth)
|
|
540
|
+
const maxScroll = Math.max(0, rows.length - bodyHeight)
|
|
541
|
+
const scrollOffset = Math.max(0, Math.min(state.scrollOffset, maxScroll))
|
|
542
|
+
const visibleRows = rows.slice(scrollOffset, scrollOffset + bodyHeight)
|
|
543
|
+
|
|
544
|
+
return (
|
|
545
|
+
<ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[2, modalHeight - 4]}>
|
|
546
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
547
|
+
<TextLine>
|
|
548
|
+
<span fg={colors.accent} attributes={TextAttributes.BOLD}>{title}</span>
|
|
549
|
+
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
550
|
+
<span fg={colors.muted}>{countText}</span>
|
|
551
|
+
</TextLine>
|
|
552
|
+
</box>
|
|
553
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
554
|
+
<PlainLine text={fitCell(anchorLabel, contentWidth)} fg={colors.muted} />
|
|
555
|
+
</box>
|
|
556
|
+
<Divider width={innerWidth} />
|
|
557
|
+
<box height={bodyHeight} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
558
|
+
{visibleRows.length === 0 ? (
|
|
559
|
+
<PlainLine text={fitCell("No comments on this line.", contentWidth)} fg={colors.muted} />
|
|
560
|
+
) : visibleRows.map((row) => (
|
|
561
|
+
<PlainLine key={row.key} text={fitCell(row.text, contentWidth)} fg={row.fg} bold={row.bold ?? false} />
|
|
562
|
+
))}
|
|
563
|
+
</box>
|
|
564
|
+
<Divider width={innerWidth} />
|
|
565
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
566
|
+
<TextLine>
|
|
567
|
+
<span fg={colors.count}>↑↓</span>
|
|
568
|
+
<span fg={colors.muted}> scroll </span>
|
|
569
|
+
<span fg={colors.count}>a</span>
|
|
570
|
+
<span fg={colors.muted}> comment </span>
|
|
571
|
+
<span fg={colors.count}>esc</span>
|
|
572
|
+
<span fg={colors.muted}> close</span>
|
|
573
|
+
</TextLine>
|
|
574
|
+
</box>
|
|
575
|
+
</ModalFrame>
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
|
|
265
579
|
export const ThemeModal = ({
|
|
266
580
|
state,
|
|
267
581
|
activeThemeId,
|
package/src/ui/pullRequests.ts
CHANGED
|
@@ -18,6 +18,8 @@ export const checkLabel = (pullRequest: PullRequestItem) => pullRequest.checkSum
|
|
|
18
18
|
export const statusColor = (status: PullRequestItem["reviewStatus"] | PullRequestItem["checkStatus"]) => colors.status[status]
|
|
19
19
|
|
|
20
20
|
export const reviewIcon = (pullRequest: PullRequestItem) => {
|
|
21
|
+
if (pullRequest.state === "merged") return "✓"
|
|
22
|
+
if (pullRequest.state === "closed") return "×"
|
|
21
23
|
if (pullRequest.autoMergeEnabled) return "↻"
|
|
22
24
|
if (pullRequest.reviewStatus === "draft") return "◌"
|
|
23
25
|
if (pullRequest.reviewStatus === "approved") return "✓"
|