@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,299 @@
|
|
|
1
|
+
import { pullRequestPartsStaleAfter } from "@reviewflow/sdk";
|
|
2
|
+
import type {
|
|
3
|
+
DiffWhitespace,
|
|
4
|
+
PullRequestDetailResponse,
|
|
5
|
+
PullRequestEdit,
|
|
6
|
+
PullRequestRead,
|
|
7
|
+
PullRequestRef,
|
|
8
|
+
} from "@reviewflow/sdk";
|
|
9
|
+
import {
|
|
10
|
+
ExternalLinkButton,
|
|
11
|
+
IconButton,
|
|
12
|
+
IndeterminateLinearProgress,
|
|
13
|
+
Text,
|
|
14
|
+
View,
|
|
15
|
+
WarningMessage,
|
|
16
|
+
} from "alouette";
|
|
17
|
+
import { ArrowClockwiseRegularIcon } from "alouette-icons/phosphor-icons/ArrowClockwise";
|
|
18
|
+
import type { ReactNode } from "react";
|
|
19
|
+
import { useMemo } from "react";
|
|
20
|
+
import { usePullRequestDataSource } from "../dataSource.ts";
|
|
21
|
+
import type { DraftStore } from "../services/PullRequestDraftsProvider.tsx";
|
|
22
|
+
import { PullRequestDraftsProvider } from "../services/PullRequestDraftsProvider.tsx";
|
|
23
|
+
import { PullRequestReviewDraftProvider } from "../services/PullRequestReviewDraftProvider.tsx";
|
|
24
|
+
import { PullRequestSuggestionBatchProvider } from "../services/PullRequestSuggestionBatchProvider.tsx";
|
|
25
|
+
import { formatDataAge } from "../services/formatDataAge.ts";
|
|
26
|
+
import { GithubImagesContext } from "../services/githubImages.tsx";
|
|
27
|
+
import type { PullRequestSlots } from "../slots.tsx";
|
|
28
|
+
import { PullRequestSlotsProvider } from "../slots.tsx";
|
|
29
|
+
import type { DiffTheme } from "./branch-diff/diff-themes.ts";
|
|
30
|
+
import type { DiffFileOrder } from "./branch-diff/use-diff-settings.tsx";
|
|
31
|
+
import { DiffSettingsProvider } from "./branch-diff/use-diff-settings.tsx";
|
|
32
|
+
import type { PullRequestTab } from "./pull-request-panel.tsx";
|
|
33
|
+
import {
|
|
34
|
+
PullRequestPanel,
|
|
35
|
+
PullRequestSummaryHeader,
|
|
36
|
+
} from "./pull-request-panel.tsx";
|
|
37
|
+
import {
|
|
38
|
+
PartReadingScope,
|
|
39
|
+
PullRequestPartPending,
|
|
40
|
+
usePartReadingCount,
|
|
41
|
+
} from "./pull-request-part-notice.tsx";
|
|
42
|
+
import { RepositoryAttachmentBanner } from "./repository-attachment-banner.tsx";
|
|
43
|
+
|
|
44
|
+
/** How old what the header shows is: GitHub's answer, or the last read shown meanwhile. */
|
|
45
|
+
const readTimeText = (
|
|
46
|
+
read: Pick<PullRequestRead, "fetchedAt"> | null,
|
|
47
|
+
cached: boolean,
|
|
48
|
+
): string => {
|
|
49
|
+
if (read === null) return "";
|
|
50
|
+
const at = formatDataAge(read.fetchedAt);
|
|
51
|
+
return cached
|
|
52
|
+
? `last read at ${at}, reading GitHub…`
|
|
53
|
+
: `read from GitHub at ${at}`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** A data source with no summary holds nothing to head the screen with. */
|
|
57
|
+
const useNoSummary = (): null => null;
|
|
58
|
+
|
|
59
|
+
/** A data source that does not say whether a checkout is attached has no banner to show. */
|
|
60
|
+
const useNoAttachment = (): null => null;
|
|
61
|
+
|
|
62
|
+
interface PullRequestBodyProps {
|
|
63
|
+
ownerRepo: string;
|
|
64
|
+
number: number;
|
|
65
|
+
headerStart?: ReactNode;
|
|
66
|
+
tab: PullRequestTab;
|
|
67
|
+
onTabChange: (tab: PullRequestTab) => void;
|
|
68
|
+
openCommit?: string | null;
|
|
69
|
+
onOpenCommitChange?: (oid: string | null) => void;
|
|
70
|
+
renderBelowHeader?: PullRequestScreenProps["renderBelowHeader"];
|
|
71
|
+
renderHeaderActions?: PullRequestScreenProps["renderHeaderActions"];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function PullRequestBody({
|
|
75
|
+
ownerRepo,
|
|
76
|
+
number,
|
|
77
|
+
headerStart,
|
|
78
|
+
tab,
|
|
79
|
+
onTabChange,
|
|
80
|
+
openCommit,
|
|
81
|
+
onOpenCommitChange,
|
|
82
|
+
renderBelowHeader,
|
|
83
|
+
renderHeaderActions,
|
|
84
|
+
}: PullRequestBodyProps): ReactNode {
|
|
85
|
+
const dataSource = usePullRequestDataSource();
|
|
86
|
+
const { usePart, useCandidates, reload } = dataSource;
|
|
87
|
+
const useSummary = dataSource.useSummary ?? useNoSummary;
|
|
88
|
+
const ref: PullRequestRef = { ownerRepo, number };
|
|
89
|
+
const summary = useSummary(ref);
|
|
90
|
+
const attachment = (dataSource.useRepositoryAttachment ?? useNoAttachment)(
|
|
91
|
+
ref,
|
|
92
|
+
);
|
|
93
|
+
const detailState = usePart(ref, "detail");
|
|
94
|
+
const checks = usePart(ref, "checks");
|
|
95
|
+
const timeline = usePart(ref, "timeline");
|
|
96
|
+
const { value: candidates, error: candidatesError } = useCandidates(ref);
|
|
97
|
+
const { value: detail, cached, error } = detailState;
|
|
98
|
+
const partsReadInTabs = usePartReadingCount();
|
|
99
|
+
const reading =
|
|
100
|
+
detailState.reading ||
|
|
101
|
+
checks.reading ||
|
|
102
|
+
timeline.reading ||
|
|
103
|
+
partsReadInTabs.count > 0;
|
|
104
|
+
const githubUrl = detail?.pullRequest.url ?? summary?.url ?? null;
|
|
105
|
+
const detailImages = detail?.images;
|
|
106
|
+
const timelineImages = timeline.value?.images;
|
|
107
|
+
const images = useMemo(
|
|
108
|
+
() => ({ ...detailImages, ...timelineImages }),
|
|
109
|
+
[detailImages, timelineImages],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const onEdited = async (edit: PullRequestEdit): Promise<void> => {
|
|
113
|
+
await reload(
|
|
114
|
+
ref,
|
|
115
|
+
detail === null
|
|
116
|
+
? undefined
|
|
117
|
+
: pullRequestPartsStaleAfter(edit, detail.pullRequest.id),
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const latest =
|
|
122
|
+
detail === null || cached
|
|
123
|
+
? null
|
|
124
|
+
: {
|
|
125
|
+
head: detail.pullRequest.headSha,
|
|
126
|
+
base: detail.pullRequest.baseSha,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<View className="gap-m">
|
|
131
|
+
<View className="flex-row items-center gap-xs">
|
|
132
|
+
{headerStart}
|
|
133
|
+
<Text className="min-w-0 flex-1 text-right text-sm text-muted">
|
|
134
|
+
{readTimeText(detail, cached)}
|
|
135
|
+
</Text>
|
|
136
|
+
{renderHeaderActions?.(ref, detail)}
|
|
137
|
+
<IconButton
|
|
138
|
+
icon={<ArrowClockwiseRegularIcon />}
|
|
139
|
+
size="sm"
|
|
140
|
+
variant="soft"
|
|
141
|
+
aria-label="Read the pull request again"
|
|
142
|
+
disabled={reading}
|
|
143
|
+
onPress={() => {
|
|
144
|
+
reload(ref).catch(() => undefined);
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
{githubUrl === null ? null : (
|
|
148
|
+
<ExternalLinkButton
|
|
149
|
+
text="Open on GitHub"
|
|
150
|
+
accent="neutral"
|
|
151
|
+
variant="tonal"
|
|
152
|
+
size="sm"
|
|
153
|
+
href={githubUrl}
|
|
154
|
+
openLinkBehavior={{ native: "webBrowser", web: "targetBlank" }}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
</View>
|
|
158
|
+
{/* any part being read, the first time as over the last read shown, the tabs' too */}
|
|
159
|
+
<IndeterminateLinearProgress loading={reading} size="xs" />
|
|
160
|
+
{renderBelowHeader?.(ref, detail)}
|
|
161
|
+
{attachment === null ? null : (
|
|
162
|
+
<RepositoryAttachmentBanner
|
|
163
|
+
ownerRepo={ownerRepo}
|
|
164
|
+
attachment={attachment}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
{error === null ? null : <WarningMessage>{error}</WarningMessage>}
|
|
168
|
+
{detail === null && error === null ? (
|
|
169
|
+
<View className="gap-l">
|
|
170
|
+
{summary === null ? null : (
|
|
171
|
+
<PullRequestSummaryHeader summary={summary} />
|
|
172
|
+
)}
|
|
173
|
+
<PullRequestPartPending error={null} what="the pull request" />
|
|
174
|
+
</View>
|
|
175
|
+
) : null}
|
|
176
|
+
{detail === null ? null : (
|
|
177
|
+
<GithubImagesContext value={images}>
|
|
178
|
+
<PullRequestReviewDraftProvider latest={latest}>
|
|
179
|
+
<PullRequestSuggestionBatchProvider
|
|
180
|
+
pullRequest={detail.pullRequest}
|
|
181
|
+
onEdited={onEdited}
|
|
182
|
+
>
|
|
183
|
+
<PartReadingScope value={partsReadInTabs.report}>
|
|
184
|
+
<PullRequestPanel
|
|
185
|
+
detail={detail}
|
|
186
|
+
candidates={candidates}
|
|
187
|
+
candidatesError={candidatesError}
|
|
188
|
+
tab={tab}
|
|
189
|
+
openCommit={openCommit}
|
|
190
|
+
onEdited={onEdited}
|
|
191
|
+
onTabChange={onTabChange}
|
|
192
|
+
onOpenCommitChange={onOpenCommitChange}
|
|
193
|
+
/>
|
|
194
|
+
</PartReadingScope>
|
|
195
|
+
</PullRequestSuggestionBatchProvider>
|
|
196
|
+
</PullRequestReviewDraftProvider>
|
|
197
|
+
</GithubImagesContext>
|
|
198
|
+
)}
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface PullRequestScreenProps {
|
|
204
|
+
ownerRepo: string;
|
|
205
|
+
number: number;
|
|
206
|
+
/** the tab shown; controlled, so an app can keep it in its url */
|
|
207
|
+
tab: PullRequestTab;
|
|
208
|
+
onTabChange: (tab: PullRequestTab) => void;
|
|
209
|
+
/**
|
|
210
|
+
* The commit whose diff is open on the Commits tab, by oid. Absent: the
|
|
211
|
+
* screen holds it itself; pass both to keep it in a url.
|
|
212
|
+
*/
|
|
213
|
+
openCommit?: string | null;
|
|
214
|
+
onOpenCommitChange?: (oid: string | null) => void;
|
|
215
|
+
/** at the start of the top line, before the read time: a back button */
|
|
216
|
+
headerStart?: ReactNode;
|
|
217
|
+
/**
|
|
218
|
+
* Under the top line, where an app shows what it knows about this pull
|
|
219
|
+
* request that GitHub does not (the task carrying it, say). Called with
|
|
220
|
+
* `null` until the detail part has arrived; render nothing if it needs it.
|
|
221
|
+
*/
|
|
222
|
+
renderBelowHeader?: (
|
|
223
|
+
ref: PullRequestRef,
|
|
224
|
+
detail: PullRequestDetailResponse | null,
|
|
225
|
+
) => ReactNode;
|
|
226
|
+
/**
|
|
227
|
+
* Buttons at the end of the top line, before the refresh button (an app's
|
|
228
|
+
* own action on the pull request). Called with `null` until the detail
|
|
229
|
+
* part has arrived.
|
|
230
|
+
*/
|
|
231
|
+
renderHeaderActions?: (
|
|
232
|
+
ref: PullRequestRef,
|
|
233
|
+
detail: PullRequestDetailResponse | null,
|
|
234
|
+
) => ReactNode;
|
|
235
|
+
/** what the app replaces, see {@link PullRequestSlots} */
|
|
236
|
+
slots?: PullRequestSlots;
|
|
237
|
+
/**
|
|
238
|
+
* Where comments and the review being written are kept between visits.
|
|
239
|
+
* Absent: they live as long as the screen.
|
|
240
|
+
*/
|
|
241
|
+
draftStore?: DraftStore;
|
|
242
|
+
/** the diff's syntax theme family; default `github` */
|
|
243
|
+
diffTheme?: DiffTheme;
|
|
244
|
+
/** wrap long lines in the diff; default true */
|
|
245
|
+
diffWordWrap?: boolean;
|
|
246
|
+
/** whether the diff starts with whitespace changes hidden; the toolbar toggle then owns it */
|
|
247
|
+
initialWhitespace?: DiffWhitespace;
|
|
248
|
+
/** the order of the diff's files an app stored; absent, the toolbar toggle owns it */
|
|
249
|
+
diffFileOrder?: DiffFileOrder;
|
|
250
|
+
/** called by the toolbar toggle, for an app that stores the order */
|
|
251
|
+
onDiffFileOrderChange?: (fileOrder: DiffFileOrder) => void;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* One pull request laid out as GitHub's page: the header, then the tabs
|
|
256
|
+
* Conversation, Commits, Checks and Files changed, with the edits a reviewer
|
|
257
|
+
* or an author makes. Reads and writes through the
|
|
258
|
+
* {@link PullRequestDataSource} of the nearest
|
|
259
|
+
* `PullRequestDataSourceProvider`, which the app provides. Mount it with a
|
|
260
|
+
* `key` of the pull request, so a draft review never leaks to the next one.
|
|
261
|
+
*/
|
|
262
|
+
export function PullRequestScreen({
|
|
263
|
+
ownerRepo,
|
|
264
|
+
number,
|
|
265
|
+
slots = {},
|
|
266
|
+
draftStore,
|
|
267
|
+
diffTheme,
|
|
268
|
+
diffWordWrap,
|
|
269
|
+
initialWhitespace,
|
|
270
|
+
diffFileOrder,
|
|
271
|
+
onDiffFileOrderChange,
|
|
272
|
+
...body
|
|
273
|
+
}: PullRequestScreenProps): ReactNode {
|
|
274
|
+
return (
|
|
275
|
+
<PullRequestSlotsProvider value={slots}>
|
|
276
|
+
<DiffSettingsProvider
|
|
277
|
+
theme={diffTheme}
|
|
278
|
+
wordWrap={diffWordWrap}
|
|
279
|
+
initialWhitespace={initialWhitespace}
|
|
280
|
+
fileOrder={diffFileOrder}
|
|
281
|
+
onFileOrderChange={onDiffFileOrderChange}
|
|
282
|
+
>
|
|
283
|
+
<View className="mx-auto w-full max-w-[1200px] gap-m px-l pt-l pb-xl">
|
|
284
|
+
<PullRequestDraftsProvider
|
|
285
|
+
store={draftStore}
|
|
286
|
+
pullRequest={{ ownerRepo, number }}
|
|
287
|
+
>
|
|
288
|
+
<PullRequestBody
|
|
289
|
+
key={`${ownerRepo}#${String(number)}`}
|
|
290
|
+
ownerRepo={ownerRepo}
|
|
291
|
+
number={number}
|
|
292
|
+
{...body}
|
|
293
|
+
/>
|
|
294
|
+
</PullRequestDraftsProvider>
|
|
295
|
+
</View>
|
|
296
|
+
</DiffSettingsProvider>
|
|
297
|
+
</PullRequestSlotsProvider>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseSuggestions,
|
|
3
|
+
suggestionCommitRefusal,
|
|
4
|
+
suggestionRangesOverlap,
|
|
5
|
+
} from "@reviewflow/sdk";
|
|
6
|
+
import type {
|
|
7
|
+
PullRequestComment,
|
|
8
|
+
PullRequestSuggestionRef,
|
|
9
|
+
PullRequestThread,
|
|
10
|
+
} from "@reviewflow/sdk";
|
|
11
|
+
import {
|
|
12
|
+
Button,
|
|
13
|
+
FormItem,
|
|
14
|
+
InputText,
|
|
15
|
+
QuestionAlertDialog,
|
|
16
|
+
Text,
|
|
17
|
+
TextArea,
|
|
18
|
+
View,
|
|
19
|
+
} from "alouette";
|
|
20
|
+
import type { ReactNode } from "react";
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import type {
|
|
23
|
+
BatchedSuggestion,
|
|
24
|
+
SuggestionCommitMessage,
|
|
25
|
+
} from "../services/PullRequestSuggestionBatchProvider.tsx";
|
|
26
|
+
import { useSuggestionBatch } from "../services/PullRequestSuggestionBatchProvider.tsx";
|
|
27
|
+
import { errorToMessage } from "../services/errorToMessage.ts";
|
|
28
|
+
|
|
29
|
+
const defaultHeadline = (count: number): string =>
|
|
30
|
+
count === 1
|
|
31
|
+
? "Apply suggestion from code review"
|
|
32
|
+
: "Apply suggestions from code review";
|
|
33
|
+
|
|
34
|
+
interface CommitMessageFieldsProps {
|
|
35
|
+
value: SuggestionCommitMessage;
|
|
36
|
+
onChange: (value: SuggestionCommitMessage) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function CommitMessageFields({
|
|
40
|
+
value,
|
|
41
|
+
onChange,
|
|
42
|
+
}: CommitMessageFieldsProps): ReactNode {
|
|
43
|
+
return (
|
|
44
|
+
<View className="gap-m">
|
|
45
|
+
<FormItem
|
|
46
|
+
label="Commit message"
|
|
47
|
+
render={({ labelId, describedBy, invalid, required }) => (
|
|
48
|
+
<InputText
|
|
49
|
+
aria-labelledby={labelId}
|
|
50
|
+
aria-describedby={describedBy}
|
|
51
|
+
aria-required={required}
|
|
52
|
+
invalid={invalid}
|
|
53
|
+
value={value.headline}
|
|
54
|
+
onChangeText={(headline) => {
|
|
55
|
+
onChange({ ...value, headline });
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
/>
|
|
60
|
+
<FormItem
|
|
61
|
+
label="Extended description"
|
|
62
|
+
render={({ labelId, describedBy, invalid, required }) => (
|
|
63
|
+
<TextArea
|
|
64
|
+
aria-labelledby={labelId}
|
|
65
|
+
aria-describedby={describedBy}
|
|
66
|
+
aria-required={required}
|
|
67
|
+
invalid={invalid}
|
|
68
|
+
value={value.body}
|
|
69
|
+
numberOfLines={4}
|
|
70
|
+
onChangeText={(body) => {
|
|
71
|
+
onChange({ ...value, body });
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
/>
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface SuggestionActionsProps {
|
|
81
|
+
thread: PullRequestThread;
|
|
82
|
+
comment: PullRequestComment;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Commit a comment's suggestion, alone or with the batch. The refusal shown
|
|
87
|
+
* on the buttons is read from the screen's last read; the daemon checks
|
|
88
|
+
* GitHub's current state again before committing.
|
|
89
|
+
*/
|
|
90
|
+
export function SuggestionActions({
|
|
91
|
+
thread,
|
|
92
|
+
comment,
|
|
93
|
+
}: SuggestionActionsProps): ReactNode {
|
|
94
|
+
const batch = useSuggestionBatch();
|
|
95
|
+
const [message, setMessage] = useState<SuggestionCommitMessage | null>(null);
|
|
96
|
+
const [committing, setCommitting] = useState<PullRequestSuggestionRef[]>([]);
|
|
97
|
+
const suggestionCount = parseSuggestions(comment.body).length;
|
|
98
|
+
if (batch === null || suggestionCount === 0) return null;
|
|
99
|
+
|
|
100
|
+
const { pullRequest } = batch;
|
|
101
|
+
const refusal = suggestionCommitRefusal({
|
|
102
|
+
state: pullRequest.state,
|
|
103
|
+
viewerCanApplySuggestion: pullRequest.viewerCanApplySuggestion,
|
|
104
|
+
suggestionCount,
|
|
105
|
+
outdated: thread.outdated,
|
|
106
|
+
line: thread.line,
|
|
107
|
+
side: thread.side,
|
|
108
|
+
startSide: thread.startSide,
|
|
109
|
+
});
|
|
110
|
+
const own: BatchedSuggestion | null =
|
|
111
|
+
refusal !== null || thread.line === null
|
|
112
|
+
? null
|
|
113
|
+
: {
|
|
114
|
+
threadId: thread.id,
|
|
115
|
+
commentId: comment.id,
|
|
116
|
+
path: thread.path,
|
|
117
|
+
startLine: thread.startLine,
|
|
118
|
+
line: thread.line,
|
|
119
|
+
};
|
|
120
|
+
const inBatch = batch.batch.some(({ commentId }) => commentId === comment.id);
|
|
121
|
+
const addRefusal = ((): string | undefined => {
|
|
122
|
+
if (own === null) return refusal ?? undefined;
|
|
123
|
+
if (batch.batch.some((batched) => suggestionRangesOverlap(batched, own))) {
|
|
124
|
+
return "A suggestion of the batch already replaces these lines";
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
})();
|
|
128
|
+
const toCommit = batch.batch.length > 0 || own === null ? batch.batch : [own];
|
|
129
|
+
|
|
130
|
+
const open = (): void => {
|
|
131
|
+
if (toCommit.length === 0) return;
|
|
132
|
+
setCommitting(toCommit);
|
|
133
|
+
setMessage({ headline: defaultHeadline(toCommit.length), body: "" });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<View className="flex-row flex-wrap items-center justify-end gap-xs">
|
|
138
|
+
{inBatch ? (
|
|
139
|
+
<Button
|
|
140
|
+
size="sm"
|
|
141
|
+
accent="neutral"
|
|
142
|
+
variant="soft"
|
|
143
|
+
text="Remove from batch"
|
|
144
|
+
onPress={() => {
|
|
145
|
+
batch.remove(comment.id);
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
) : (
|
|
149
|
+
<Button
|
|
150
|
+
size="sm"
|
|
151
|
+
accent="neutral"
|
|
152
|
+
text="Add suggestion to batch"
|
|
153
|
+
disabledReason={addRefusal}
|
|
154
|
+
onPress={() => {
|
|
155
|
+
if (own !== null) batch.add(own);
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
<Button
|
|
160
|
+
size="sm"
|
|
161
|
+
text={
|
|
162
|
+
batch.batch.length === 0
|
|
163
|
+
? "Commit suggestion"
|
|
164
|
+
: `Commit ${String(batch.batch.length)} suggestion${batch.batch.length === 1 ? "" : "s"}`
|
|
165
|
+
}
|
|
166
|
+
disabledReason={
|
|
167
|
+
toCommit.length === 0 ? (refusal ?? undefined) : undefined
|
|
168
|
+
}
|
|
169
|
+
onPress={open}
|
|
170
|
+
/>
|
|
171
|
+
<QuestionAlertDialog
|
|
172
|
+
visible={message !== null}
|
|
173
|
+
title={
|
|
174
|
+
committing.length === 1
|
|
175
|
+
? "Commit this suggestion?"
|
|
176
|
+
: `Commit ${String(committing.length)} suggestions?`
|
|
177
|
+
}
|
|
178
|
+
confirmText="Commit changes"
|
|
179
|
+
confirmDisabled={message === null || message.headline.trim() === ""}
|
|
180
|
+
errorToMessage={errorToMessage}
|
|
181
|
+
onConfirm={async () => {
|
|
182
|
+
if (message === null) return;
|
|
183
|
+
await batch.commit(committing, message);
|
|
184
|
+
setMessage(null);
|
|
185
|
+
}}
|
|
186
|
+
onCancel={() => {
|
|
187
|
+
setMessage(null);
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
<View className="gap-m">
|
|
191
|
+
<Text>
|
|
192
|
+
{`One commit on ${pullRequest.headRef}, on GitHub, under your login.`}
|
|
193
|
+
</Text>
|
|
194
|
+
{message === null ? null : (
|
|
195
|
+
<CommitMessageFields value={message} onChange={setMessage} />
|
|
196
|
+
)}
|
|
197
|
+
</View>
|
|
198
|
+
</QuestionAlertDialog>
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
}
|