@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,533 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PullRequestCommit,
|
|
3
|
+
PullRequestDetail,
|
|
4
|
+
PullRequestDetailResponse,
|
|
5
|
+
PullRequestEdit,
|
|
6
|
+
PullRequestThread,
|
|
7
|
+
PullRequestViewerLastReview,
|
|
8
|
+
} from "@reviewflow/sdk";
|
|
9
|
+
import {
|
|
10
|
+
Button,
|
|
11
|
+
Code,
|
|
12
|
+
IconButton,
|
|
13
|
+
InfoMessage,
|
|
14
|
+
Menu,
|
|
15
|
+
MenuItem,
|
|
16
|
+
Tab,
|
|
17
|
+
Tabs,
|
|
18
|
+
Text,
|
|
19
|
+
View,
|
|
20
|
+
WarningMessage,
|
|
21
|
+
} from "alouette";
|
|
22
|
+
import { CaretLeftRegularIcon } from "alouette-icons/phosphor-icons/CaretLeft";
|
|
23
|
+
import { CaretRightRegularIcon } from "alouette-icons/phosphor-icons/CaretRight";
|
|
24
|
+
import { GitCommitRegularIcon } from "alouette-icons/phosphor-icons/GitCommit";
|
|
25
|
+
import type { ReactNode } from "react";
|
|
26
|
+
import { useMemo, useState } from "react";
|
|
27
|
+
import { usePullRequestDataSource } from "../dataSource.ts";
|
|
28
|
+
import { usePullRequestReviewDraft } from "../services/PullRequestReviewDraftProvider.tsx";
|
|
29
|
+
import { errorToMessage } from "../services/errorToMessage.ts";
|
|
30
|
+
import { formatRelativeDate } from "../services/formatRelativeDate.ts";
|
|
31
|
+
import { isViewerAuthor } from "../services/isViewerAuthor.ts";
|
|
32
|
+
import { PullRequestReviewButton } from "./pull-request-actions-review.tsx";
|
|
33
|
+
import type {
|
|
34
|
+
CommitSummary,
|
|
35
|
+
CommitsToRead,
|
|
36
|
+
} from "./pull-request-commit-files.ts";
|
|
37
|
+
import {
|
|
38
|
+
commitAnchorings,
|
|
39
|
+
commitsToRead,
|
|
40
|
+
firstCommitAfter,
|
|
41
|
+
summarizeCommits,
|
|
42
|
+
touchDifferentFiles,
|
|
43
|
+
} from "./pull-request-commit-files.ts";
|
|
44
|
+
import type { PullRequestMovedOn } from "./pull-request-diff-data.ts";
|
|
45
|
+
import {
|
|
46
|
+
changedPathsOf,
|
|
47
|
+
useMissingCheckoutReason,
|
|
48
|
+
usePullRequestMovedOn,
|
|
49
|
+
} from "./pull-request-diff-data.ts";
|
|
50
|
+
import { PullRequestDiffView } from "./pull-request-diff-view.tsx";
|
|
51
|
+
import type { PullRequestDiffReview } from "./pull-request-diff-viewer.tsx";
|
|
52
|
+
import {
|
|
53
|
+
PullRequestPartError,
|
|
54
|
+
PullRequestPartPending,
|
|
55
|
+
PullRequestPartReading,
|
|
56
|
+
} from "./pull-request-part-notice.tsx";
|
|
57
|
+
|
|
58
|
+
type Scope = "all" | "since";
|
|
59
|
+
|
|
60
|
+
interface MovedOnNoticeProps {
|
|
61
|
+
movedOn: PullRequestMovedOn | null;
|
|
62
|
+
follow: (() => void) | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function MovedOnNotice({ movedOn, follow }: MovedOnNoticeProps): ReactNode {
|
|
66
|
+
if (movedOn === null) return null;
|
|
67
|
+
if (follow === null) {
|
|
68
|
+
return (
|
|
69
|
+
<InfoMessage>
|
|
70
|
+
The pull request moved on while your review is in progress. The diff
|
|
71
|
+
stays on the commit the review started on until it is sent or no comment
|
|
72
|
+
is pending or being written. Files changed since cannot be marked
|
|
73
|
+
viewed, and their comments are listed above them rather than on a line.
|
|
74
|
+
</InfoMessage>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return (
|
|
78
|
+
<InfoMessage>
|
|
79
|
+
<View className="gap-xs">
|
|
80
|
+
<Text>
|
|
81
|
+
The pull request moved on since this diff was shown. Files changed
|
|
82
|
+
since cannot be marked viewed, and their comments are listed above
|
|
83
|
+
them rather than on a line.
|
|
84
|
+
</Text>
|
|
85
|
+
<View className="flex-row">
|
|
86
|
+
<Button
|
|
87
|
+
text="Show the latest changes"
|
|
88
|
+
accent="neutral"
|
|
89
|
+
variant="tonal"
|
|
90
|
+
size="sm"
|
|
91
|
+
onPress={follow}
|
|
92
|
+
/>
|
|
93
|
+
</View>
|
|
94
|
+
</View>
|
|
95
|
+
</InfoMessage>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function LastReviewRange({
|
|
100
|
+
scope,
|
|
101
|
+
lastReview,
|
|
102
|
+
}: {
|
|
103
|
+
scope: Scope;
|
|
104
|
+
lastReview: PullRequestViewerLastReview | null;
|
|
105
|
+
}): ReactNode {
|
|
106
|
+
if (scope !== "since" || lastReview === null) return null;
|
|
107
|
+
return (
|
|
108
|
+
<View className="flex-row items-center gap-xs">
|
|
109
|
+
<Text className="text-sm text-muted">from your review on</Text>
|
|
110
|
+
<Code className="text-xs">{lastReview.oid.slice(0, 7)}</Code>
|
|
111
|
+
{lastReview.submittedAt === null ? null : (
|
|
112
|
+
<Text className="text-sm text-muted">
|
|
113
|
+
{formatRelativeDate(lastReview.submittedAt)}
|
|
114
|
+
</Text>
|
|
115
|
+
)}
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface ByCommitButtonProps {
|
|
121
|
+
byCommit: boolean;
|
|
122
|
+
disabledReason: string | null;
|
|
123
|
+
onToggle: () => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A toggle: brand while the commits are read one by one, as the whitespace one. */
|
|
127
|
+
function ByCommitButton({
|
|
128
|
+
byCommit,
|
|
129
|
+
disabledReason,
|
|
130
|
+
onToggle,
|
|
131
|
+
}: ByCommitButtonProps): ReactNode {
|
|
132
|
+
return (
|
|
133
|
+
<IconButton
|
|
134
|
+
icon={<GitCommitRegularIcon />}
|
|
135
|
+
size="sm"
|
|
136
|
+
accent={byCommit ? "brand" : "neutral"}
|
|
137
|
+
disabledReason={disabledReason}
|
|
138
|
+
aria-label={
|
|
139
|
+
byCommit ? "Show all the changes at once" : "Read commit by commit"
|
|
140
|
+
}
|
|
141
|
+
onPress={onToggle}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const describeCommit = (
|
|
147
|
+
commit: PullRequestCommit,
|
|
148
|
+
summary: CommitSummary | undefined,
|
|
149
|
+
): string => {
|
|
150
|
+
const oid = commit.oid.slice(0, 7);
|
|
151
|
+
if (summary === undefined) return oid;
|
|
152
|
+
const files = `${String(summary.files)} ${summary.files === 1 ? "file" : "files"}`;
|
|
153
|
+
return [oid, summary.workspaces.join(", "), files]
|
|
154
|
+
.filter((part) => part !== "")
|
|
155
|
+
.join(" · ");
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
interface CommitStepperProps {
|
|
159
|
+
commits: PullRequestCommit[];
|
|
160
|
+
/** aligned with `commits` */
|
|
161
|
+
summaries: CommitSummary[];
|
|
162
|
+
index: number;
|
|
163
|
+
onIndexChange: (index: number) => void;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function CommitStepper({
|
|
167
|
+
commits,
|
|
168
|
+
summaries,
|
|
169
|
+
index,
|
|
170
|
+
onIndexChange,
|
|
171
|
+
}: CommitStepperProps): ReactNode {
|
|
172
|
+
const commit = commits[index];
|
|
173
|
+
if (commit === undefined) return null;
|
|
174
|
+
return (
|
|
175
|
+
<View className="min-w-0 shrink flex-row items-center gap-xs">
|
|
176
|
+
<IconButton
|
|
177
|
+
icon={<CaretLeftRegularIcon />}
|
|
178
|
+
size="sm"
|
|
179
|
+
accent="neutral"
|
|
180
|
+
aria-label="Previous commit"
|
|
181
|
+
disabledReason={index === 0 ? "This is the first commit" : null}
|
|
182
|
+
onPress={() => {
|
|
183
|
+
onIndexChange(index - 1);
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
<Menu
|
|
187
|
+
label="Commits"
|
|
188
|
+
render={(triggerProps) => (
|
|
189
|
+
<Button
|
|
190
|
+
text={`${String(index + 1)} / ${String(commits.length)}`}
|
|
191
|
+
accent="neutral"
|
|
192
|
+
variant="soft"
|
|
193
|
+
size="sm"
|
|
194
|
+
{...triggerProps}
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{commits.map((item, itemIndex) => (
|
|
199
|
+
<MenuItem
|
|
200
|
+
key={item.oid}
|
|
201
|
+
label={item.headline}
|
|
202
|
+
description={describeCommit(item, summaries[itemIndex])}
|
|
203
|
+
checked={itemIndex === index}
|
|
204
|
+
onPress={() => {
|
|
205
|
+
onIndexChange(itemIndex);
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</Menu>
|
|
210
|
+
<IconButton
|
|
211
|
+
icon={<CaretRightRegularIcon />}
|
|
212
|
+
size="sm"
|
|
213
|
+
accent="neutral"
|
|
214
|
+
aria-label="Next commit"
|
|
215
|
+
disabledReason={
|
|
216
|
+
index === commits.length - 1 ? "This is the last commit" : null
|
|
217
|
+
}
|
|
218
|
+
onPress={() => {
|
|
219
|
+
onIndexChange(index + 1);
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
<Text className="min-w-0 shrink text-sm font-body-bold" numberOfLines={1}>
|
|
223
|
+
{commit.headline}
|
|
224
|
+
</Text>
|
|
225
|
+
<Code className="text-xs">{commit.oid.slice(0, 7)}</Code>
|
|
226
|
+
</View>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface CommitByCommitParams {
|
|
231
|
+
pullRequest: PullRequestDetail;
|
|
232
|
+
/** the head the diff shows, null until known */
|
|
233
|
+
head: string | null;
|
|
234
|
+
/** step through the commits after this one only */
|
|
235
|
+
since: string | null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface CommitByCommit {
|
|
239
|
+
/** null while the commits are read */
|
|
240
|
+
toRead: CommitsToRead | null;
|
|
241
|
+
/** why the reader cannot turn it on */
|
|
242
|
+
disabledReason: string | null;
|
|
243
|
+
on: boolean;
|
|
244
|
+
/** the default is not known yet: decided before the diff shows, so it does not swap under the reader */
|
|
245
|
+
waiting: boolean;
|
|
246
|
+
/** why the commits' files could not be read, once the reader asked for them */
|
|
247
|
+
error: string | null;
|
|
248
|
+
stepped: PullRequestCommit[];
|
|
249
|
+
/** aligned with `stepped` */
|
|
250
|
+
summaries: CommitSummary[];
|
|
251
|
+
index: number;
|
|
252
|
+
setIndex: (index: number) => void;
|
|
253
|
+
shown: {
|
|
254
|
+
commit: PullRequestCommit;
|
|
255
|
+
files: NonNullable<PullRequestDiffReview["commitFiles"]>;
|
|
256
|
+
} | null;
|
|
257
|
+
toggle: () => void;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const useCommitByCommit = ({
|
|
261
|
+
pullRequest,
|
|
262
|
+
head,
|
|
263
|
+
since,
|
|
264
|
+
}: CommitByCommitParams): CommitByCommit => {
|
|
265
|
+
const dataSource = usePullRequestDataSource();
|
|
266
|
+
const commits = dataSource.usePart(pullRequest, "commits");
|
|
267
|
+
const missingCheckout = useMissingCheckoutReason(pullRequest);
|
|
268
|
+
const [requested, setRequested] = useState<boolean | null>(null);
|
|
269
|
+
const [requestedIndex, setRequestedIndex] = useState(0);
|
|
270
|
+
const toRead =
|
|
271
|
+
head === null || commits.value === null
|
|
272
|
+
? null
|
|
273
|
+
: commitsToRead({
|
|
274
|
+
listed: commits.value.commits,
|
|
275
|
+
total: pullRequest.commitsTotal,
|
|
276
|
+
head,
|
|
277
|
+
});
|
|
278
|
+
const readable =
|
|
279
|
+
toRead?.kind === "available" && missingCheckout === null
|
|
280
|
+
? toRead.commits
|
|
281
|
+
: null;
|
|
282
|
+
const diffs = dataSource.useDiffs(
|
|
283
|
+
pullRequest,
|
|
284
|
+
readable === null || requested === false
|
|
285
|
+
? null
|
|
286
|
+
: readable.map(({ oid, parentOid }) => ({
|
|
287
|
+
head: oid,
|
|
288
|
+
base: parentOid ?? oid,
|
|
289
|
+
// a file changed by whitespace alone is changed all the same
|
|
290
|
+
whitespace: "show",
|
|
291
|
+
})),
|
|
292
|
+
);
|
|
293
|
+
// a cached value is the previous ranges', kept while these load
|
|
294
|
+
const pathsByCommit = useMemo(
|
|
295
|
+
() => (diffs.cached ? null : (diffs.value?.map(changedPathsOf) ?? null)),
|
|
296
|
+
[diffs.cached, diffs.value],
|
|
297
|
+
);
|
|
298
|
+
const wanted =
|
|
299
|
+
requested ??
|
|
300
|
+
(pathsByCommit === null ? null : touchDifferentFiles(pathsByCommit));
|
|
301
|
+
const failed = diffs.error !== null || diffs.refused !== null;
|
|
302
|
+
const on = readable !== null && pathsByCommit !== null && wanted === true;
|
|
303
|
+
const sinceIndex = readable === null ? 0 : firstCommitAfter(readable, since);
|
|
304
|
+
const stepped = readable?.slice(sinceIndex) ?? [];
|
|
305
|
+
const index = Math.min(requestedIndex, Math.max(stepped.length - 1, 0));
|
|
306
|
+
const commit = on ? stepped[index] : undefined;
|
|
307
|
+
const summaries =
|
|
308
|
+
on && diffs.value !== null && pathsByCommit !== null
|
|
309
|
+
? summarizeCommits({ diffs: diffs.value, pathsByCommit, sinceIndex })
|
|
310
|
+
: [];
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
toRead,
|
|
314
|
+
disabledReason:
|
|
315
|
+
toRead?.kind === "unavailable" ? toRead.reason : missingCheckout,
|
|
316
|
+
on,
|
|
317
|
+
waiting:
|
|
318
|
+
readable !== null &&
|
|
319
|
+
wanted !== false &&
|
|
320
|
+
pathsByCommit === null &&
|
|
321
|
+
!failed,
|
|
322
|
+
error: requested === true ? diffs.error : null,
|
|
323
|
+
stepped,
|
|
324
|
+
summaries,
|
|
325
|
+
index,
|
|
326
|
+
setIndex: setRequestedIndex,
|
|
327
|
+
shown:
|
|
328
|
+
commit === undefined || pathsByCommit === null
|
|
329
|
+
? null
|
|
330
|
+
: {
|
|
331
|
+
commit,
|
|
332
|
+
files: {
|
|
333
|
+
oid: commit.oid,
|
|
334
|
+
anchorings: commitAnchorings(pathsByCommit, sinceIndex + index),
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
toggle: () => {
|
|
338
|
+
setRequested(!on);
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The pull request's diff with its review state: viewed marks, threads on the
|
|
345
|
+
* lines and commenting. Scope "since your last review" diffs the head against
|
|
346
|
+
* the head the viewer last reviewed on; GitHub's line numbers for the old side
|
|
347
|
+
* then name the pull request's base, not that commit, so only the new side is
|
|
348
|
+
* annotated or commentable there.
|
|
349
|
+
*
|
|
350
|
+
* Review changes is the one place a review is sent from, so it ends the diff's
|
|
351
|
+
* toolbar, which sticks to the top of the scroll container. The author gets
|
|
352
|
+
* neither it nor Start a review — GitHub lets them comment, one comment at a
|
|
353
|
+
* time, not review.
|
|
354
|
+
*
|
|
355
|
+
* The range is the draft's: a push is announced, not shown, until the reader
|
|
356
|
+
* asks for it, which a review in progress withholds.
|
|
357
|
+
*
|
|
358
|
+
* The commits can be read one by one, which is the default when no two of
|
|
359
|
+
* them change the same file: each file of a commit is then the pull request's
|
|
360
|
+
* own, so it is reviewed as on the whole diff. A file changed by another
|
|
361
|
+
* commit too is reviewed on what it shares with the pull request's diff
|
|
362
|
+
* (`CommitFileAnchoring`). With "since your last review", only the commits
|
|
363
|
+
* pushed since are stepped through. Off without a local checkout: from
|
|
364
|
+
* GitHub, telling the default would cost a request a commit.
|
|
365
|
+
*/
|
|
366
|
+
export interface PullRequestFilesTabProps {
|
|
367
|
+
detail: PullRequestDetailResponse;
|
|
368
|
+
/** reads the pull request again once GitHub took a change */
|
|
369
|
+
onEdited: (edit: PullRequestEdit) => Promise<void>;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function PullRequestFiles({
|
|
373
|
+
detail,
|
|
374
|
+
onEdited,
|
|
375
|
+
}: PullRequestFilesTabProps): ReactNode {
|
|
376
|
+
const { pullRequest } = detail;
|
|
377
|
+
const isAuthor = isViewerAuthor(detail);
|
|
378
|
+
const [requested, setRequested] = useState<Scope>("all");
|
|
379
|
+
const [viewedError, setViewedError] = useState<string | null>(null);
|
|
380
|
+
const dataSource = usePullRequestDataSource();
|
|
381
|
+
const files = dataSource.usePart(pullRequest, "files");
|
|
382
|
+
const timeline = dataSource.usePart(pullRequest, "timeline");
|
|
383
|
+
const { shas, latest, follow } = usePullRequestReviewDraft();
|
|
384
|
+
const movedOn = usePullRequestMovedOn(pullRequest, shas, latest);
|
|
385
|
+
const lastReview = files.value?.viewerLastReview ?? null;
|
|
386
|
+
const sinceAvailable = lastReview !== null && lastReview.oid !== shas?.head;
|
|
387
|
+
const scope: Scope = sinceAvailable ? requested : "all";
|
|
388
|
+
const byCommit = useCommitByCommit({
|
|
389
|
+
pullRequest,
|
|
390
|
+
head: shas?.head ?? null,
|
|
391
|
+
since: scope === "since" ? (lastReview?.oid ?? null) : null,
|
|
392
|
+
});
|
|
393
|
+
const shownCommit = byCommit.shown?.commit;
|
|
394
|
+
const commitFiles = byCommit.shown?.files;
|
|
395
|
+
const viewedCount = (files.value?.files ?? []).filter(
|
|
396
|
+
({ viewed }) => viewed === "viewed",
|
|
397
|
+
).length;
|
|
398
|
+
|
|
399
|
+
const onEdit = async (edit: PullRequestEdit): Promise<void> => {
|
|
400
|
+
await dataSource.edit(pullRequest, edit);
|
|
401
|
+
await onEdited(edit);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<View className="gap-m">
|
|
406
|
+
{viewedError === null ? null : (
|
|
407
|
+
<WarningMessage
|
|
408
|
+
dismissIconAriaLabel="Dismiss"
|
|
409
|
+
onDismiss={() => {
|
|
410
|
+
setViewedError(null);
|
|
411
|
+
}}
|
|
412
|
+
>
|
|
413
|
+
{viewedError}
|
|
414
|
+
</WarningMessage>
|
|
415
|
+
)}
|
|
416
|
+
<PullRequestPartError error={files.error ?? timeline.error} />
|
|
417
|
+
{files.value === null || timeline.value === null ? (
|
|
418
|
+
<PullRequestPartPending
|
|
419
|
+
error={null}
|
|
420
|
+
what="the viewed marks and threads"
|
|
421
|
+
/>
|
|
422
|
+
) : (
|
|
423
|
+
<PullRequestPartReading reading={files.reading || timeline.reading} />
|
|
424
|
+
)}
|
|
425
|
+
<MovedOnNotice movedOn={movedOn} follow={follow} />
|
|
426
|
+
{byCommit.error === null ? null : (
|
|
427
|
+
<WarningMessage>
|
|
428
|
+
{`The commits' files could not be read: ${byCommit.error}`}
|
|
429
|
+
</WarningMessage>
|
|
430
|
+
)}
|
|
431
|
+
{(() => {
|
|
432
|
+
if (shas === null) {
|
|
433
|
+
return (
|
|
434
|
+
<PullRequestPartPending
|
|
435
|
+
error={null}
|
|
436
|
+
what="the pull request's head"
|
|
437
|
+
/>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (byCommit.waiting) {
|
|
441
|
+
return (
|
|
442
|
+
<PullRequestPartPending
|
|
443
|
+
error={null}
|
|
444
|
+
what="the files each commit changes"
|
|
445
|
+
/>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return (
|
|
449
|
+
<PullRequestDiffView
|
|
450
|
+
range={{
|
|
451
|
+
ownerRepo: pullRequest.ownerRepo,
|
|
452
|
+
number: pullRequest.number,
|
|
453
|
+
head: shownCommit?.oid ?? shas.head,
|
|
454
|
+
base: (() => {
|
|
455
|
+
if (shownCommit !== undefined) {
|
|
456
|
+
return shownCommit.parentOid ?? shas.base;
|
|
457
|
+
}
|
|
458
|
+
if (scope === "since" && lastReview !== null) {
|
|
459
|
+
return lastReview.oid;
|
|
460
|
+
}
|
|
461
|
+
return shas.base;
|
|
462
|
+
})(),
|
|
463
|
+
}}
|
|
464
|
+
toolbarStart={
|
|
465
|
+
<>
|
|
466
|
+
{byCommit.toRead === null ||
|
|
467
|
+
byCommit.toRead.kind === "none" ? null : (
|
|
468
|
+
<ByCommitButton
|
|
469
|
+
byCommit={byCommit.on}
|
|
470
|
+
disabledReason={byCommit.disabledReason}
|
|
471
|
+
onToggle={byCommit.toggle}
|
|
472
|
+
/>
|
|
473
|
+
)}
|
|
474
|
+
{sinceAvailable ? (
|
|
475
|
+
<Tabs
|
|
476
|
+
aria-label="Changes shown"
|
|
477
|
+
value={scope}
|
|
478
|
+
onValueChange={(value) => {
|
|
479
|
+
setRequested(value as Scope);
|
|
480
|
+
byCommit.setIndex(0);
|
|
481
|
+
}}
|
|
482
|
+
>
|
|
483
|
+
<Tab value="all" label="All changes" />
|
|
484
|
+
<Tab value="since" label="Since your last review" />
|
|
485
|
+
</Tabs>
|
|
486
|
+
) : null}
|
|
487
|
+
<LastReviewRange scope={scope} lastReview={lastReview} />
|
|
488
|
+
<Text className="text-sm text-muted">
|
|
489
|
+
{`${String(viewedCount)} / ${String(pullRequest.changedFiles)} viewed`}
|
|
490
|
+
</Text>
|
|
491
|
+
{byCommit.shown === null ? null : (
|
|
492
|
+
<CommitStepper
|
|
493
|
+
commits={byCommit.stepped}
|
|
494
|
+
summaries={byCommit.summaries}
|
|
495
|
+
index={byCommit.index}
|
|
496
|
+
onIndexChange={byCommit.setIndex}
|
|
497
|
+
/>
|
|
498
|
+
)}
|
|
499
|
+
</>
|
|
500
|
+
}
|
|
501
|
+
toolbarEnd={
|
|
502
|
+
isAuthor ? null : (
|
|
503
|
+
<PullRequestReviewButton detail={detail} onEdit={onEdit} />
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
review={{
|
|
507
|
+
headSha: shas.head,
|
|
508
|
+
threads:
|
|
509
|
+
timeline.value?.timeline.filter(
|
|
510
|
+
(item): item is PullRequestThread => item.kind === "thread",
|
|
511
|
+
) ?? null,
|
|
512
|
+
files: files.value?.files ?? null,
|
|
513
|
+
commentable: true,
|
|
514
|
+
rightOnly: scope === "since" && commitFiles === undefined,
|
|
515
|
+
commitFiles,
|
|
516
|
+
reviewable: !isAuthor,
|
|
517
|
+
movedOn,
|
|
518
|
+
onEdit,
|
|
519
|
+
onViewed: async (path, viewed) => {
|
|
520
|
+
try {
|
|
521
|
+
await onEdit({ kind: "viewed", path, viewed });
|
|
522
|
+
setViewedError(null);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
setViewedError(errorToMessage(error));
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
}}
|
|
528
|
+
/>
|
|
529
|
+
);
|
|
530
|
+
})()}
|
|
531
|
+
</View>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Box, Tab, Tabs, Text, View } from "alouette";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { MarkdownEditor } from "../editor/markdown-editor.tsx";
|
|
5
|
+
import type { MarkdownEditorProps } from "../slots.tsx";
|
|
6
|
+
import { usePullRequestSlots } from "../slots.tsx";
|
|
7
|
+
import { MarkdownText } from "./markdown-text.tsx";
|
|
8
|
+
|
|
9
|
+
type Mode = "preview" | "write";
|
|
10
|
+
|
|
11
|
+
interface PullRequestMarkdownInputProps {
|
|
12
|
+
value: string;
|
|
13
|
+
onChange: (value: string) => void;
|
|
14
|
+
"aria-label": string;
|
|
15
|
+
autoFocus?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function DefaultEditor({
|
|
19
|
+
value,
|
|
20
|
+
onChange,
|
|
21
|
+
"aria-label": ariaLabel,
|
|
22
|
+
autoFocus,
|
|
23
|
+
}: MarkdownEditorProps): ReactNode {
|
|
24
|
+
return (
|
|
25
|
+
<MarkdownEditor
|
|
26
|
+
compact
|
|
27
|
+
emoji
|
|
28
|
+
aria-label={ariaLabel}
|
|
29
|
+
// oxlint-disable-next-line jsx-a11y/no-autofocus -- the caller opens the editor from a press and asks for the focus
|
|
30
|
+
autoFocus={autoFocus}
|
|
31
|
+
value={value}
|
|
32
|
+
onChange={onChange}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A component of its own, so a slot that uses hooks keeps them across tab switches. */
|
|
38
|
+
function EditorSlot(props: MarkdownEditorProps): ReactNode {
|
|
39
|
+
const { renderMarkdownEditor } = usePullRequestSlots();
|
|
40
|
+
return renderMarkdownEditor === undefined ? (
|
|
41
|
+
<DefaultEditor {...props} />
|
|
42
|
+
) : (
|
|
43
|
+
renderMarkdownEditor(props)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A body GitHub renders as markdown, written as GitHub's own box is: the
|
|
49
|
+
* source in the editor slot, and a preview rendered the way the timeline
|
|
50
|
+
* shows it once sent.
|
|
51
|
+
*/
|
|
52
|
+
export function PullRequestMarkdownInput({
|
|
53
|
+
value,
|
|
54
|
+
onChange,
|
|
55
|
+
"aria-label": ariaLabel,
|
|
56
|
+
autoFocus,
|
|
57
|
+
}: PullRequestMarkdownInputProps): ReactNode {
|
|
58
|
+
const [mode, setMode] = useState<Mode>("write");
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View className="gap-xs">
|
|
62
|
+
<Tabs
|
|
63
|
+
aria-label={`${ariaLabel}, write or preview`}
|
|
64
|
+
value={mode}
|
|
65
|
+
onValueChange={(next) => {
|
|
66
|
+
setMode(next === "preview" ? "preview" : "write");
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<Tab value="write" label="Write" />
|
|
70
|
+
<Tab value="preview" label="Preview" />
|
|
71
|
+
</Tabs>
|
|
72
|
+
{mode === "write" ? (
|
|
73
|
+
<EditorSlot
|
|
74
|
+
// oxlint-disable-next-line jsx-a11y/no-autofocus -- forwarded to the editor, see above
|
|
75
|
+
autoFocus={autoFocus}
|
|
76
|
+
aria-label={ariaLabel}
|
|
77
|
+
value={value}
|
|
78
|
+
onChange={onChange}
|
|
79
|
+
/>
|
|
80
|
+
) : (
|
|
81
|
+
<Box className="surface lowered min-h-[96px] p-m">
|
|
82
|
+
{value.trim() === "" ? (
|
|
83
|
+
<Text className="text-sm text-muted">Nothing to preview.</Text>
|
|
84
|
+
) : (
|
|
85
|
+
<MarkdownText github text={value} />
|
|
86
|
+
)}
|
|
87
|
+
</Box>
|
|
88
|
+
)}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
}
|