@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.
Files changed (92) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +95 -2
  3. package/package.json +102 -4
  4. package/src/board/bucket-section.tsx +94 -0
  5. package/src/board/dataSource.ts +44 -0
  6. package/src/board/facts.ts +183 -0
  7. package/src/board/index.ts +12 -0
  8. package/src/board/pull-request-board.tsx +239 -0
  9. package/src/board/pull-request-row.tsx +181 -0
  10. package/src/components/branch-diff/diff-themes.ts +6 -0
  11. package/src/components/branch-diff/diff-worker-pool.tsx +12 -0
  12. package/src/components/branch-diff/diff-worker-pool.web.tsx +56 -0
  13. package/src/components/branch-diff/diff-worker.ts +7 -0
  14. package/src/components/branch-diff/use-collapsed-files.ts +49 -0
  15. package/src/components/branch-diff/use-diff-settings.tsx +97 -0
  16. package/src/components/branch-diff/whitespace-button.tsx +32 -0
  17. package/src/components/copyable-code-block.tsx +101 -0
  18. package/src/components/label-pill.tsx +84 -0
  19. package/src/components/link-in-row.ts +6 -0
  20. package/src/components/markdown-suggestion.tsx +60 -0
  21. package/src/components/markdown-text.tsx +1067 -0
  22. package/src/components/pull-request-actions-merge.tsx +341 -0
  23. package/src/components/pull-request-actions-review.tsx +218 -0
  24. package/src/components/pull-request-checks.tsx +86 -0
  25. package/src/components/pull-request-commit-files.ts +143 -0
  26. package/src/components/pull-request-commits.tsx +221 -0
  27. package/src/components/pull-request-conversation-card.tsx +220 -0
  28. package/src/components/pull-request-conversation-edit.tsx +147 -0
  29. package/src/components/pull-request-conversation-event.tsx +267 -0
  30. package/src/components/pull-request-conversation-person.tsx +28 -0
  31. package/src/components/pull-request-conversation-thread.tsx +158 -0
  32. package/src/components/pull-request-diff-annotations.tsx +443 -0
  33. package/src/components/pull-request-diff-collapse.ts +43 -0
  34. package/src/components/pull-request-diff-data.ts +174 -0
  35. package/src/components/pull-request-diff-impacts-summary.tsx +97 -0
  36. package/src/components/pull-request-diff-impacts.ts +45 -0
  37. package/src/components/pull-request-diff-jump.ts +87 -0
  38. package/src/components/pull-request-diff-lines.ts +103 -0
  39. package/src/components/pull-request-diff-overview.tsx +416 -0
  40. package/src/components/pull-request-diff-view.tsx +438 -0
  41. package/src/components/pull-request-diff-viewer.tsx +1374 -0
  42. package/src/components/pull-request-files.tsx +533 -0
  43. package/src/components/pull-request-markdown-input.tsx +91 -0
  44. package/src/components/pull-request-panel.tsx +1270 -0
  45. package/src/components/pull-request-part-notice.tsx +82 -0
  46. package/src/components/pull-request-reactions.tsx +178 -0
  47. package/src/components/pull-request-review.tsx +164 -0
  48. package/src/components/pull-request-revisions.tsx +273 -0
  49. package/src/components/pull-request-screen.tsx +299 -0
  50. package/src/components/pull-request-suggestion-actions.tsx +201 -0
  51. package/src/components/pull-request-timeline.tsx +270 -0
  52. package/src/components/repository-attachment-banner.tsx +42 -0
  53. package/src/components/scroll-parent.ts +50 -0
  54. package/src/css.d.ts +1 -0
  55. package/src/dataSource.ts +242 -0
  56. package/src/editor/alerts.ts +105 -0
  57. package/src/editor/code-editor.native.tsx +25 -0
  58. package/src/editor/code-editor.tsx +112 -0
  59. package/src/editor/codeEditorExtensions.ts +65 -0
  60. package/src/editor/codeHighlight.ts +71 -0
  61. package/src/editor/commands.ts +175 -0
  62. package/src/editor/decorations.ts +425 -0
  63. package/src/editor/editable-file.tsx +224 -0
  64. package/src/editor/emojiCompletion.ts +110 -0
  65. package/src/editor/extensions.ts +149 -0
  66. package/src/editor/frontmatter.ts +76 -0
  67. package/src/editor/inlineFormat.ts +114 -0
  68. package/src/editor/lineTypes.ts +55 -0
  69. package/src/editor/markdown-editor.css +511 -0
  70. package/src/editor/markdown-editor.native.tsx +29 -0
  71. package/src/editor/markdown-editor.tsx +295 -0
  72. package/src/editor/nodeClasses.ts +69 -0
  73. package/src/index.ts +75 -0
  74. package/src/markdown-github/flatten.ts +49 -0
  75. package/src/markdown-github/html-tokens.ts +93 -0
  76. package/src/markdown-github/pair-html.ts +354 -0
  77. package/src/markdown-github/safe-url.ts +15 -0
  78. package/src/markdown-github/table-columns.ts +23 -0
  79. package/src/markdown-github/toggle-task.ts +164 -0
  80. package/src/nativewind-env.d.ts +1 -0
  81. package/src/prStatus.ts +90 -0
  82. package/src/services/PullRequestDraftsProvider.tsx +179 -0
  83. package/src/services/PullRequestReviewDraftProvider.tsx +178 -0
  84. package/src/services/PullRequestSuggestionBatchProvider.tsx +112 -0
  85. package/src/services/emojify.ts +13 -0
  86. package/src/services/errorToMessage.ts +6 -0
  87. package/src/services/expiringDraftStore.ts +102 -0
  88. package/src/services/formatDataAge.ts +12 -0
  89. package/src/services/formatRelativeDate.ts +56 -0
  90. package/src/services/githubImages.tsx +33 -0
  91. package/src/services/isViewerAuthor.ts +7 -0
  92. 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
+ }