@reviewflow/ui 0.0.0-stage → 6.21.0
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/LICENSE +15 -0
- package/README.md +95 -2
- package/package.json +102 -4
- package/src/board/bucket-section.tsx +94 -0
- package/src/board/dataSource.ts +44 -0
- package/src/board/facts.ts +183 -0
- package/src/board/index.ts +12 -0
- package/src/board/pull-request-board.tsx +239 -0
- package/src/board/pull-request-row.tsx +181 -0
- package/src/components/branch-diff/diff-themes.ts +6 -0
- package/src/components/branch-diff/diff-worker-pool.tsx +12 -0
- package/src/components/branch-diff/diff-worker-pool.web.tsx +56 -0
- package/src/components/branch-diff/diff-worker.ts +7 -0
- package/src/components/branch-diff/use-collapsed-files.ts +49 -0
- package/src/components/branch-diff/use-diff-settings.tsx +97 -0
- package/src/components/branch-diff/whitespace-button.tsx +32 -0
- package/src/components/copyable-code-block.tsx +101 -0
- package/src/components/label-pill.tsx +84 -0
- package/src/components/link-in-row.ts +6 -0
- package/src/components/markdown-suggestion.tsx +60 -0
- package/src/components/markdown-text.tsx +1067 -0
- package/src/components/pull-request-actions-merge.tsx +341 -0
- package/src/components/pull-request-actions-review.tsx +218 -0
- package/src/components/pull-request-checks.tsx +86 -0
- package/src/components/pull-request-commit-files.ts +143 -0
- package/src/components/pull-request-commits.tsx +221 -0
- package/src/components/pull-request-conversation-card.tsx +220 -0
- package/src/components/pull-request-conversation-edit.tsx +147 -0
- package/src/components/pull-request-conversation-event.tsx +267 -0
- package/src/components/pull-request-conversation-person.tsx +28 -0
- package/src/components/pull-request-conversation-thread.tsx +158 -0
- package/src/components/pull-request-diff-annotations.tsx +443 -0
- package/src/components/pull-request-diff-collapse.ts +43 -0
- package/src/components/pull-request-diff-data.ts +174 -0
- package/src/components/pull-request-diff-impacts-summary.tsx +97 -0
- package/src/components/pull-request-diff-impacts.ts +45 -0
- package/src/components/pull-request-diff-jump.ts +87 -0
- package/src/components/pull-request-diff-lines.ts +103 -0
- package/src/components/pull-request-diff-overview.tsx +416 -0
- package/src/components/pull-request-diff-view.tsx +438 -0
- package/src/components/pull-request-diff-viewer.tsx +1374 -0
- package/src/components/pull-request-files.tsx +533 -0
- package/src/components/pull-request-markdown-input.tsx +91 -0
- package/src/components/pull-request-panel.tsx +1270 -0
- package/src/components/pull-request-part-notice.tsx +82 -0
- package/src/components/pull-request-reactions.tsx +178 -0
- package/src/components/pull-request-review.tsx +164 -0
- package/src/components/pull-request-revisions.tsx +273 -0
- package/src/components/pull-request-screen.tsx +299 -0
- package/src/components/pull-request-suggestion-actions.tsx +201 -0
- package/src/components/pull-request-timeline.tsx +270 -0
- package/src/components/repository-attachment-banner.tsx +42 -0
- package/src/components/scroll-parent.ts +50 -0
- package/src/css.d.ts +1 -0
- package/src/dataSource.ts +242 -0
- package/src/editor/alerts.ts +105 -0
- package/src/editor/code-editor.native.tsx +25 -0
- package/src/editor/code-editor.tsx +112 -0
- package/src/editor/codeEditorExtensions.ts +65 -0
- package/src/editor/codeHighlight.ts +71 -0
- package/src/editor/commands.ts +175 -0
- package/src/editor/decorations.ts +425 -0
- package/src/editor/editable-file.tsx +224 -0
- package/src/editor/emojiCompletion.ts +110 -0
- package/src/editor/extensions.ts +149 -0
- package/src/editor/frontmatter.ts +76 -0
- package/src/editor/inlineFormat.ts +114 -0
- package/src/editor/lineTypes.ts +55 -0
- package/src/editor/markdown-editor.css +511 -0
- package/src/editor/markdown-editor.native.tsx +29 -0
- package/src/editor/markdown-editor.tsx +295 -0
- package/src/editor/nodeClasses.ts +69 -0
- package/src/index.ts +75 -0
- package/src/markdown-github/flatten.ts +49 -0
- package/src/markdown-github/html-tokens.ts +93 -0
- package/src/markdown-github/pair-html.ts +354 -0
- package/src/markdown-github/safe-url.ts +15 -0
- package/src/markdown-github/table-columns.ts +23 -0
- package/src/markdown-github/toggle-task.ts +164 -0
- package/src/nativewind-env.d.ts +1 -0
- package/src/prStatus.ts +90 -0
- package/src/services/PullRequestDraftsProvider.tsx +179 -0
- package/src/services/PullRequestReviewDraftProvider.tsx +178 -0
- package/src/services/PullRequestSuggestionBatchProvider.tsx +112 -0
- package/src/services/emojify.ts +13 -0
- package/src/services/errorToMessage.ts +6 -0
- package/src/services/expiringDraftStore.ts +102 -0
- package/src/services/formatDataAge.ts +12 -0
- package/src/services/formatRelativeDate.ts +56 -0
- package/src/services/githubImages.tsx +33 -0
- package/src/services/isViewerAuthor.ts +7 -0
- package/src/slots.tsx +34 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { PullRequestDetail, PullRequestEdit } from "@reviewflow/sdk";
|
|
2
|
+
import { ActionButton, Button, Text, View } from "alouette";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { usePullRequestDataSource } from "../dataSource.ts";
|
|
5
|
+
import { useDraftText } from "../services/PullRequestDraftsProvider.tsx";
|
|
6
|
+
import { errorToMessage } from "../services/errorToMessage.ts";
|
|
7
|
+
import { PullRequestMarkdownInput } from "./pull-request-markdown-input.tsx";
|
|
8
|
+
|
|
9
|
+
/** What every gesture of the conversation needs to write and to re-read. */
|
|
10
|
+
export interface ConversationEditing {
|
|
11
|
+
pullRequest: PullRequestDetail;
|
|
12
|
+
/** reads again the parts of the pull request the change made stale */
|
|
13
|
+
onEdited: (edit: PullRequestEdit) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Writes the edit through the data source, then reads again what it made stale. */
|
|
17
|
+
export const useSendEdit = ({
|
|
18
|
+
pullRequest,
|
|
19
|
+
onEdited,
|
|
20
|
+
}: ConversationEditing): ((edit: PullRequestEdit) => Promise<void>) => {
|
|
21
|
+
const { edit: writeEdit } = usePullRequestDataSource();
|
|
22
|
+
return async (edit) => {
|
|
23
|
+
await writeEdit(pullRequest, edit);
|
|
24
|
+
await onEdited(edit);
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interface BodyEditorProps {
|
|
29
|
+
editing: ConversationEditing;
|
|
30
|
+
/** where the edit in progress is kept, see `useDraftText` */
|
|
31
|
+
draftKey: string;
|
|
32
|
+
initial: string;
|
|
33
|
+
ariaLabel: string;
|
|
34
|
+
toEdit: (body: string) => PullRequestEdit;
|
|
35
|
+
onDone: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The inline editor of a description or a comment: the source, with its preview. */
|
|
39
|
+
export function BodyEditor({
|
|
40
|
+
editing,
|
|
41
|
+
draftKey,
|
|
42
|
+
initial,
|
|
43
|
+
ariaLabel,
|
|
44
|
+
toEdit,
|
|
45
|
+
onDone,
|
|
46
|
+
}: BodyEditorProps): ReactNode {
|
|
47
|
+
const sendEdit = useSendEdit(editing);
|
|
48
|
+
const draft = useDraftText(draftKey, initial);
|
|
49
|
+
return (
|
|
50
|
+
<View className="gap-xs">
|
|
51
|
+
<PullRequestMarkdownInput
|
|
52
|
+
aria-label={ariaLabel}
|
|
53
|
+
value={draft.text}
|
|
54
|
+
onChange={draft.setText}
|
|
55
|
+
/>
|
|
56
|
+
<View className="flex-row flex-wrap items-center justify-end gap-xs">
|
|
57
|
+
<Button
|
|
58
|
+
text="Cancel"
|
|
59
|
+
accent="neutral"
|
|
60
|
+
variant="soft"
|
|
61
|
+
size="sm"
|
|
62
|
+
onPress={() => {
|
|
63
|
+
draft.discard();
|
|
64
|
+
onDone();
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
<ActionButton
|
|
68
|
+
text="Save"
|
|
69
|
+
accent="brand"
|
|
70
|
+
variant="tonal"
|
|
71
|
+
size="sm"
|
|
72
|
+
errorToMessage={errorToMessage}
|
|
73
|
+
onPress={async () => {
|
|
74
|
+
const sent = draft.text;
|
|
75
|
+
await sendEdit(toEdit(sent));
|
|
76
|
+
draft.discard(sent);
|
|
77
|
+
onDone();
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
</View>
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ComposerDraft {
|
|
86
|
+
body: string;
|
|
87
|
+
clear: () => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface ComposerProps {
|
|
91
|
+
editing: ConversationEditing;
|
|
92
|
+
/** where the text being written is kept, see `useDraftText` */
|
|
93
|
+
draftKey: string;
|
|
94
|
+
title?: string;
|
|
95
|
+
ariaLabel: string;
|
|
96
|
+
buttonText: string;
|
|
97
|
+
toEdit: (body: string) => PullRequestEdit;
|
|
98
|
+
/** before the submit button, e.g. Resolve under a reply */
|
|
99
|
+
extraActions?: (draft: ComposerDraft) => ReactNode;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A new comment or a reply: cleared once GitHub took it, kept when it refused. */
|
|
103
|
+
export function Composer({
|
|
104
|
+
editing,
|
|
105
|
+
draftKey,
|
|
106
|
+
title,
|
|
107
|
+
ariaLabel,
|
|
108
|
+
buttonText,
|
|
109
|
+
toEdit,
|
|
110
|
+
extraActions,
|
|
111
|
+
}: ComposerProps): ReactNode {
|
|
112
|
+
const sendEdit = useSendEdit(editing);
|
|
113
|
+
const draft = useDraftText(draftKey);
|
|
114
|
+
return (
|
|
115
|
+
<View className="gap-xs">
|
|
116
|
+
{title === undefined ? null : (
|
|
117
|
+
<Text className="font-body-bold text-sm">{title}</Text>
|
|
118
|
+
)}
|
|
119
|
+
<PullRequestMarkdownInput
|
|
120
|
+
aria-label={ariaLabel}
|
|
121
|
+
value={draft.text}
|
|
122
|
+
onChange={draft.setText}
|
|
123
|
+
/>
|
|
124
|
+
<View className="flex-row flex-wrap items-center justify-end gap-xs">
|
|
125
|
+
{extraActions?.({
|
|
126
|
+
body: draft.text,
|
|
127
|
+
clear: () => {
|
|
128
|
+
draft.discard();
|
|
129
|
+
},
|
|
130
|
+
})}
|
|
131
|
+
<ActionButton
|
|
132
|
+
text={buttonText}
|
|
133
|
+
accent="brand"
|
|
134
|
+
variant="tonal"
|
|
135
|
+
size="sm"
|
|
136
|
+
disabled={draft.text.trim() === ""}
|
|
137
|
+
errorToMessage={errorToMessage}
|
|
138
|
+
onPress={async () => {
|
|
139
|
+
const sent = draft.text;
|
|
140
|
+
await sendEdit(toEdit(sent));
|
|
141
|
+
draft.discard(sent);
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { PullRequestEvent } from "@reviewflow/sdk";
|
|
2
|
+
import type { IconProps } from "alouette";
|
|
3
|
+
import { Code, Icon, Text, View } from "alouette";
|
|
4
|
+
import { ArrowsClockwiseRegularIcon } from "alouette-icons/phosphor-icons/ArrowsClockwise";
|
|
5
|
+
import { CheckCircleRegularIcon } from "alouette-icons/phosphor-icons/CheckCircle";
|
|
6
|
+
import { CircleDashedRegularIcon } from "alouette-icons/phosphor-icons/CircleDashed";
|
|
7
|
+
import { EyeRegularIcon } from "alouette-icons/phosphor-icons/Eye";
|
|
8
|
+
import { FileDashedRegularIcon } from "alouette-icons/phosphor-icons/FileDashed";
|
|
9
|
+
import { GitBranchRegularIcon } from "alouette-icons/phosphor-icons/GitBranch";
|
|
10
|
+
import { GitCommitRegularIcon } from "alouette-icons/phosphor-icons/GitCommit";
|
|
11
|
+
import { GitMergeRegularIcon } from "alouette-icons/phosphor-icons/GitMerge";
|
|
12
|
+
import { LightningRegularIcon } from "alouette-icons/phosphor-icons/Lightning";
|
|
13
|
+
import { PencilSimpleRegularIcon } from "alouette-icons/phosphor-icons/PencilSimple";
|
|
14
|
+
import { ProhibitRegularIcon } from "alouette-icons/phosphor-icons/Prohibit";
|
|
15
|
+
import { TagRegularIcon } from "alouette-icons/phosphor-icons/Tag";
|
|
16
|
+
import { UserMinusRegularIcon } from "alouette-icons/phosphor-icons/UserMinus";
|
|
17
|
+
import { UserPlusRegularIcon } from "alouette-icons/phosphor-icons/UserPlus";
|
|
18
|
+
import { XCircleRegularIcon } from "alouette-icons/phosphor-icons/XCircle";
|
|
19
|
+
import type { ReactNode } from "react";
|
|
20
|
+
import { formatDataAge } from "../services/formatDataAge.ts";
|
|
21
|
+
import { LabelPill } from "./label-pill.tsx";
|
|
22
|
+
|
|
23
|
+
interface EventRowProps {
|
|
24
|
+
icon: IconProps["icon"];
|
|
25
|
+
event: PullRequestEvent;
|
|
26
|
+
/** the words before the subject */
|
|
27
|
+
text: string;
|
|
28
|
+
/** the thing the action names; a string is set bold */
|
|
29
|
+
subject?: ReactNode;
|
|
30
|
+
/** the words after it */
|
|
31
|
+
after?: string;
|
|
32
|
+
/** a second thing after `after`, e.g. the new base branch */
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function EventRow({
|
|
37
|
+
icon,
|
|
38
|
+
event,
|
|
39
|
+
text,
|
|
40
|
+
subject,
|
|
41
|
+
after,
|
|
42
|
+
children,
|
|
43
|
+
}: EventRowProps): ReactNode {
|
|
44
|
+
return (
|
|
45
|
+
<View className="min-w-0 flex-row flex-wrap items-center gap-xs pl-xs">
|
|
46
|
+
<Icon icon={icon} size={16} className="text-muted" />
|
|
47
|
+
<Text className="text-sm font-body-bold">
|
|
48
|
+
{event.actor?.login ?? "ghost"}
|
|
49
|
+
</Text>
|
|
50
|
+
<Text className="text-sm text-muted">{text}</Text>
|
|
51
|
+
{typeof subject === "string" ? (
|
|
52
|
+
<Text className="text-sm font-body-bold">{subject}</Text>
|
|
53
|
+
) : (
|
|
54
|
+
subject
|
|
55
|
+
)}
|
|
56
|
+
{after === undefined ? null : (
|
|
57
|
+
<Text className="text-sm text-muted">{after}</Text>
|
|
58
|
+
)}
|
|
59
|
+
{children}
|
|
60
|
+
<Text className="text-sm text-muted">
|
|
61
|
+
{formatDataAge(event.createdAt)}
|
|
62
|
+
</Text>
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Events about the branches and the pull request's own state. */
|
|
68
|
+
function BranchEventLine({ event }: { event: PullRequestEvent }): ReactNode {
|
|
69
|
+
const { action, subject, detail } = event;
|
|
70
|
+
if (action === "baseRefChanged") {
|
|
71
|
+
return (
|
|
72
|
+
<EventRow
|
|
73
|
+
icon={<GitBranchRegularIcon />}
|
|
74
|
+
event={event}
|
|
75
|
+
text="changed the base branch from"
|
|
76
|
+
subject={<Code>{detail ?? ""}</Code>}
|
|
77
|
+
after="to"
|
|
78
|
+
>
|
|
79
|
+
<Code>{subject ?? ""}</Code>
|
|
80
|
+
</EventRow>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (action === "headRefForcePushed") {
|
|
84
|
+
return (
|
|
85
|
+
<EventRow
|
|
86
|
+
icon={<ArrowsClockwiseRegularIcon />}
|
|
87
|
+
event={event}
|
|
88
|
+
text="force-pushed the head branch"
|
|
89
|
+
subject={detail === null ? undefined : <Code>{detail}</Code>}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (action === "merged") {
|
|
94
|
+
return (
|
|
95
|
+
<EventRow
|
|
96
|
+
icon={<GitMergeRegularIcon />}
|
|
97
|
+
event={event}
|
|
98
|
+
text="merged this pull request"
|
|
99
|
+
subject={subject ?? undefined}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (action === "closed") {
|
|
104
|
+
return (
|
|
105
|
+
<EventRow
|
|
106
|
+
icon={<XCircleRegularIcon />}
|
|
107
|
+
event={event}
|
|
108
|
+
text="closed this pull request"
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (action === "reopened") {
|
|
113
|
+
return (
|
|
114
|
+
<EventRow
|
|
115
|
+
icon={<CircleDashedRegularIcon />}
|
|
116
|
+
event={event}
|
|
117
|
+
text="reopened this pull request"
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (action === "reviewDismissed") {
|
|
122
|
+
return (
|
|
123
|
+
<EventRow
|
|
124
|
+
icon={<ProhibitRegularIcon />}
|
|
125
|
+
event={event}
|
|
126
|
+
text="dismissed a review"
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (action === "autoMergeEnabled" || action === "autoMergeDisabled") {
|
|
131
|
+
return (
|
|
132
|
+
<EventRow
|
|
133
|
+
icon={<LightningRegularIcon />}
|
|
134
|
+
event={event}
|
|
135
|
+
text={
|
|
136
|
+
action === "autoMergeEnabled"
|
|
137
|
+
? "enabled auto-merge"
|
|
138
|
+
: "disabled auto-merge"
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return (
|
|
144
|
+
<EventRow
|
|
145
|
+
icon={<GitCommitRegularIcon />}
|
|
146
|
+
event={event}
|
|
147
|
+
text="added a commit"
|
|
148
|
+
subject={subject ?? undefined}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** One event of the pull request, in GitHub's wording. */
|
|
154
|
+
export function EventLine({ event }: { event: PullRequestEvent }): ReactNode {
|
|
155
|
+
const { action, subject, detail, label } = event;
|
|
156
|
+
if (action === "reviewRequested") {
|
|
157
|
+
return (
|
|
158
|
+
<EventRow
|
|
159
|
+
icon={<EyeRegularIcon />}
|
|
160
|
+
event={event}
|
|
161
|
+
text="requested a review from"
|
|
162
|
+
subject={subject ?? undefined}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (action === "reviewRequestRemoved") {
|
|
167
|
+
return (
|
|
168
|
+
<EventRow
|
|
169
|
+
icon={<EyeRegularIcon />}
|
|
170
|
+
event={event}
|
|
171
|
+
text="removed the review request for"
|
|
172
|
+
subject={subject ?? undefined}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (action === "readyForReview") {
|
|
177
|
+
return (
|
|
178
|
+
<EventRow
|
|
179
|
+
icon={<CheckCircleRegularIcon />}
|
|
180
|
+
event={event}
|
|
181
|
+
text="marked this pull request as ready for review"
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (action === "convertedToDraft") {
|
|
186
|
+
return (
|
|
187
|
+
<EventRow
|
|
188
|
+
icon={<FileDashedRegularIcon />}
|
|
189
|
+
event={event}
|
|
190
|
+
text="marked this pull request as draft"
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (action === "labeled" || action === "unlabeled") {
|
|
195
|
+
return (
|
|
196
|
+
<EventRow
|
|
197
|
+
icon={<TagRegularIcon />}
|
|
198
|
+
event={event}
|
|
199
|
+
text={action === "labeled" ? "added the" : "removed the"}
|
|
200
|
+
subject={
|
|
201
|
+
label === null ? (subject ?? undefined) : <LabelPill label={label} />
|
|
202
|
+
}
|
|
203
|
+
after="label"
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (action === "assigned" || action === "unassigned") {
|
|
208
|
+
return (
|
|
209
|
+
<EventRow
|
|
210
|
+
icon={
|
|
211
|
+
action === "assigned" ? (
|
|
212
|
+
<UserPlusRegularIcon />
|
|
213
|
+
) : (
|
|
214
|
+
<UserMinusRegularIcon />
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
event={event}
|
|
218
|
+
text={action}
|
|
219
|
+
subject={subject ?? undefined}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (action === "renamed") {
|
|
224
|
+
return (
|
|
225
|
+
<EventRow
|
|
226
|
+
icon={<PencilSimpleRegularIcon />}
|
|
227
|
+
event={event}
|
|
228
|
+
text="changed the title"
|
|
229
|
+
subject={`${detail ?? ""} → ${subject ?? ""}`}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return <BranchEventLine event={event} />;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Consecutive commits of one actor: a line for the push, then each headline. */
|
|
237
|
+
export function CommitsLine({
|
|
238
|
+
events,
|
|
239
|
+
}: {
|
|
240
|
+
events: PullRequestEvent[];
|
|
241
|
+
}): ReactNode {
|
|
242
|
+
const [first] = events;
|
|
243
|
+
if (first === undefined) return null;
|
|
244
|
+
const count = events.length;
|
|
245
|
+
return (
|
|
246
|
+
<View className="min-w-0 gap-xxs">
|
|
247
|
+
<EventRow
|
|
248
|
+
icon={<GitCommitRegularIcon />}
|
|
249
|
+
event={first}
|
|
250
|
+
text="added"
|
|
251
|
+
subject={String(count)}
|
|
252
|
+
after={count === 1 ? "commit" : "commits"}
|
|
253
|
+
/>
|
|
254
|
+
{events.map((event, index) => (
|
|
255
|
+
<View
|
|
256
|
+
key={`${event.detail ?? ""}:${String(index)}`}
|
|
257
|
+
className="min-w-0 flex-row items-center gap-xs pl-xl"
|
|
258
|
+
>
|
|
259
|
+
<Text className="min-w-0 shrink text-sm" numberOfLines={1}>
|
|
260
|
+
{event.subject ?? ""}
|
|
261
|
+
</Text>
|
|
262
|
+
{event.detail === null ? null : <Code>{event.detail}</Code>}
|
|
263
|
+
</View>
|
|
264
|
+
))}
|
|
265
|
+
</View>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PullRequestPerson } from "@reviewflow/sdk";
|
|
2
|
+
import { Avatar } from "alouette";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { Image, View } from "react-native";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* alouette's Avatar draws initials only, so GitHub's picture is laid over it:
|
|
8
|
+
* while it loads, or when it fails, the initials are what shows.
|
|
9
|
+
*/
|
|
10
|
+
export function PersonAvatar({
|
|
11
|
+
person,
|
|
12
|
+
}: {
|
|
13
|
+
person: PullRequestPerson | null;
|
|
14
|
+
}): ReactNode {
|
|
15
|
+
const login = person?.login ?? "ghost";
|
|
16
|
+
return (
|
|
17
|
+
<View className="size-[28px] shrink-0">
|
|
18
|
+
<Avatar size="sm" name={login} />
|
|
19
|
+
{person?.avatarUrl ? (
|
|
20
|
+
<Image
|
|
21
|
+
source={{ uri: person.avatarUrl }}
|
|
22
|
+
accessibilityLabel={login}
|
|
23
|
+
className="absolute inset-0 size-[28px] rounded-full"
|
|
24
|
+
/>
|
|
25
|
+
) : null}
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { PullRequestThread } from "@reviewflow/sdk";
|
|
2
|
+
import { suggestionOriginalLines } from "@reviewflow/sdk";
|
|
3
|
+
import {
|
|
4
|
+
ActionButton,
|
|
5
|
+
Badge,
|
|
6
|
+
Box,
|
|
7
|
+
Button,
|
|
8
|
+
CodeBlock,
|
|
9
|
+
ExternalLinkText,
|
|
10
|
+
Text,
|
|
11
|
+
View,
|
|
12
|
+
} from "alouette";
|
|
13
|
+
import type { ReactNode } from "react";
|
|
14
|
+
import { useMemo, useState } from "react";
|
|
15
|
+
import { errorToMessage } from "../services/errorToMessage.ts";
|
|
16
|
+
import { SuggestionOriginalContext } from "./markdown-suggestion.tsx";
|
|
17
|
+
import { CommentCard } from "./pull-request-conversation-card.tsx";
|
|
18
|
+
import type { ConversationEditing } from "./pull-request-conversation-edit.tsx";
|
|
19
|
+
import { Composer, useSendEdit } from "./pull-request-conversation-edit.tsx";
|
|
20
|
+
import { SuggestionActions } from "./pull-request-suggestion-actions.tsx";
|
|
21
|
+
|
|
22
|
+
/** The last lines of the hunk: the ones the comment was written under. */
|
|
23
|
+
const hunkTail = (diffHunk: string): string =>
|
|
24
|
+
diffHunk
|
|
25
|
+
.split("\n")
|
|
26
|
+
.filter((line) => !line.startsWith("@@"))
|
|
27
|
+
.slice(-4)
|
|
28
|
+
.join("\n");
|
|
29
|
+
|
|
30
|
+
const lineRange = (thread: PullRequestThread): string => {
|
|
31
|
+
const line = thread.line ?? thread.originalLine;
|
|
32
|
+
if (line === null) return "";
|
|
33
|
+
if (thread.startLine !== null && thread.startLine !== line) {
|
|
34
|
+
return `:${String(thread.startLine)}-${String(line)}`;
|
|
35
|
+
}
|
|
36
|
+
return `:${String(line)}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface ThreadProps {
|
|
40
|
+
editing: ConversationEditing;
|
|
41
|
+
thread: PullRequestThread;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Resolved threads start folded, as on GitHub: they are history, not a request. */
|
|
45
|
+
export function Thread({ editing, thread }: ThreadProps): ReactNode {
|
|
46
|
+
const sendEdit = useSendEdit(editing);
|
|
47
|
+
const [open, setOpen] = useState(!thread.resolved);
|
|
48
|
+
const unlisted = thread.commentsTotal - thread.comments.length;
|
|
49
|
+
const first = thread.comments[0];
|
|
50
|
+
const last = thread.comments.at(-1);
|
|
51
|
+
const original = useMemo(() => suggestionOriginalLines(thread), [thread]);
|
|
52
|
+
|
|
53
|
+
const canToggle = thread.resolved
|
|
54
|
+
? thread.viewerCanUnresolve
|
|
55
|
+
: thread.viewerCanResolve;
|
|
56
|
+
const toggleResolved = canToggle ? (
|
|
57
|
+
<ActionButton
|
|
58
|
+
text={thread.resolved ? "Unresolve conversation" : "Resolve conversation"}
|
|
59
|
+
accent="neutral"
|
|
60
|
+
variant="tonal"
|
|
61
|
+
size="sm"
|
|
62
|
+
errorToMessage={errorToMessage}
|
|
63
|
+
onPress={async () => {
|
|
64
|
+
await sendEdit({
|
|
65
|
+
kind: "resolveThread",
|
|
66
|
+
threadId: thread.id,
|
|
67
|
+
resolved: !thread.resolved,
|
|
68
|
+
});
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
) : null;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Box className="surface min-w-0 gap-sm p-m">
|
|
75
|
+
<View className="flex-row flex-wrap items-center gap-xs">
|
|
76
|
+
<Text
|
|
77
|
+
className="min-w-0 shrink text-sm font-body-bold"
|
|
78
|
+
numberOfLines={1}
|
|
79
|
+
>
|
|
80
|
+
{thread.path + lineRange(thread)}
|
|
81
|
+
</Text>
|
|
82
|
+
{thread.outdated ? (
|
|
83
|
+
<Badge accent="warning" size="sm">
|
|
84
|
+
outdated
|
|
85
|
+
</Badge>
|
|
86
|
+
) : null}
|
|
87
|
+
{thread.resolved ? (
|
|
88
|
+
<Badge accent="success" size="sm">
|
|
89
|
+
resolved
|
|
90
|
+
</Badge>
|
|
91
|
+
) : null}
|
|
92
|
+
{thread.resolved ? (
|
|
93
|
+
<Button
|
|
94
|
+
text={open ? "Hide" : "Show"}
|
|
95
|
+
accent="neutral"
|
|
96
|
+
variant="soft"
|
|
97
|
+
size="sm"
|
|
98
|
+
onPress={() => {
|
|
99
|
+
setOpen(!open);
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
) : null}
|
|
103
|
+
</View>
|
|
104
|
+
{open ? (
|
|
105
|
+
<View className="gap-m">
|
|
106
|
+
<CodeBlock size="sm">{hunkTail(thread.diffHunk)}</CodeBlock>
|
|
107
|
+
<SuggestionOriginalContext value={original}>
|
|
108
|
+
{thread.comments.map((comment) => (
|
|
109
|
+
<CommentCard
|
|
110
|
+
key={comment.id}
|
|
111
|
+
bare
|
|
112
|
+
editing={editing}
|
|
113
|
+
author={comment.author}
|
|
114
|
+
action="commented"
|
|
115
|
+
createdAt={comment.createdAt}
|
|
116
|
+
lastEditedAt={comment.lastEditedAt}
|
|
117
|
+
url={comment.url}
|
|
118
|
+
body={comment.body}
|
|
119
|
+
canEdit={comment.viewerCanUpdate}
|
|
120
|
+
reactable={comment}
|
|
121
|
+
toEdit={(body) => ({
|
|
122
|
+
kind: "editComment",
|
|
123
|
+
target: "reviewComment",
|
|
124
|
+
databaseId: comment.databaseId,
|
|
125
|
+
body,
|
|
126
|
+
})}
|
|
127
|
+
footer={<SuggestionActions thread={thread} comment={comment} />}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
</SuggestionOriginalContext>
|
|
131
|
+
{unlisted > 0 && last !== undefined ? (
|
|
132
|
+
<ExternalLinkText
|
|
133
|
+
size="sm"
|
|
134
|
+
href={last.url}
|
|
135
|
+
text={`${String(unlisted)} more on GitHub`}
|
|
136
|
+
/>
|
|
137
|
+
) : null}
|
|
138
|
+
{thread.viewerCanReply && first !== undefined ? (
|
|
139
|
+
<Composer
|
|
140
|
+
editing={editing}
|
|
141
|
+
draftKey={`reply:${thread.id}`}
|
|
142
|
+
ariaLabel="Reply"
|
|
143
|
+
buttonText="Reply"
|
|
144
|
+
toEdit={(body) => ({
|
|
145
|
+
kind: "reply",
|
|
146
|
+
inReplyTo: first.databaseId,
|
|
147
|
+
body,
|
|
148
|
+
})}
|
|
149
|
+
extraActions={() => toggleResolved}
|
|
150
|
+
/>
|
|
151
|
+
) : (
|
|
152
|
+
toggleResolved
|
|
153
|
+
)}
|
|
154
|
+
</View>
|
|
155
|
+
) : null}
|
|
156
|
+
</Box>
|
|
157
|
+
);
|
|
158
|
+
}
|