@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,270 @@
1
+ import type {
2
+ PullRequestDetailResponse,
3
+ PullRequestEdit,
4
+ PullRequestEvent,
5
+ PullRequestTimelineItem,
6
+ } from "@reviewflow/sdk";
7
+ import { ActionButton, ExternalLinkText, View } from "alouette";
8
+ import type { ReactNode } from "react";
9
+ import { usePullRequestDataSource } from "../dataSource.ts";
10
+ import { errorToMessage } from "../services/errorToMessage.ts";
11
+ import { CommentCard } from "./pull-request-conversation-card.tsx";
12
+ import {
13
+ Composer,
14
+ type ComposerDraft,
15
+ type ConversationEditing,
16
+ useSendEdit,
17
+ } from "./pull-request-conversation-edit.tsx";
18
+ import { CommitsLine, EventLine } from "./pull-request-conversation-event.tsx";
19
+ import { Thread } from "./pull-request-conversation-thread.tsx";
20
+ import {
21
+ PullRequestPartError,
22
+ PullRequestPartPending,
23
+ PullRequestPartReading,
24
+ } from "./pull-request-part-notice.tsx";
25
+
26
+ type TimelineEntry =
27
+ | { kind: "commits"; events: PullRequestEvent[] }
28
+ | { kind: "item"; item: PullRequestTimelineItem };
29
+
30
+ const isCommit = (item: PullRequestTimelineItem): item is PullRequestEvent =>
31
+ item.kind === "event" && item.action === "committed";
32
+
33
+ /** Consecutive commits of one actor, with nothing between them, are one push. */
34
+ const groupCommits = (timeline: PullRequestTimelineItem[]): TimelineEntry[] => {
35
+ const entries: TimelineEntry[] = [];
36
+ for (const item of timeline) {
37
+ const previous = entries.at(-1);
38
+ if (
39
+ isCommit(item) &&
40
+ previous?.kind === "commits" &&
41
+ previous.events[0]?.actor?.login === item.actor?.login
42
+ ) {
43
+ previous.events.push(item);
44
+ } else if (isCommit(item)) {
45
+ entries.push({ kind: "commits", events: [item] });
46
+ } else {
47
+ entries.push({ kind: "item", item });
48
+ }
49
+ }
50
+ return entries;
51
+ };
52
+
53
+ const entryKey = (entry: TimelineEntry, index: number): string => {
54
+ if (entry.kind === "commits") {
55
+ return `commits:${entry.events[0]?.createdAt ?? ""}:${String(index)}`;
56
+ }
57
+ const { item } = entry;
58
+ if (item.kind === "event") {
59
+ return `event:${item.action}:${item.createdAt}:${String(index)}`;
60
+ }
61
+ return `${item.kind}:${item.id}`;
62
+ };
63
+
64
+ interface TimelineEntryViewProps {
65
+ editing: ConversationEditing;
66
+ entry: TimelineEntry;
67
+ }
68
+
69
+ function TimelineEntryView({
70
+ editing,
71
+ entry,
72
+ }: TimelineEntryViewProps): ReactNode {
73
+ if (entry.kind === "commits") return <CommitsLine events={entry.events} />;
74
+ const { item } = entry;
75
+ if (item.kind === "event") return <EventLine event={item} />;
76
+ if (item.kind === "thread") return <Thread editing={editing} thread={item} />;
77
+ if (item.kind === "review") {
78
+ return (
79
+ <CommentCard
80
+ editing={editing}
81
+ author={item.author}
82
+ action="reviewed"
83
+ verdict={item.state}
84
+ createdAt={item.createdAt}
85
+ lastEditedAt={item.lastEditedAt}
86
+ url={item.url}
87
+ body={item.body}
88
+ canEdit={item.viewerCanUpdate}
89
+ reactable={item}
90
+ toEdit={(body) => ({
91
+ kind: "editComment",
92
+ target: "review",
93
+ databaseId: item.databaseId,
94
+ body,
95
+ })}
96
+ />
97
+ );
98
+ }
99
+ return (
100
+ <CommentCard
101
+ editing={editing}
102
+ author={item.author}
103
+ action="commented"
104
+ createdAt={item.createdAt}
105
+ lastEditedAt={item.lastEditedAt}
106
+ url={item.url}
107
+ body={item.body}
108
+ canEdit={item.viewerCanUpdate}
109
+ reactable={item}
110
+ toEdit={(body) => ({
111
+ kind: "editComment",
112
+ target: "issueComment",
113
+ databaseId: item.databaseId,
114
+ body,
115
+ })}
116
+ />
117
+ );
118
+ }
119
+
120
+ interface CloseWithCommentProps {
121
+ editing: ConversationEditing;
122
+ draft: ComposerDraft;
123
+ }
124
+
125
+ /**
126
+ * Closing requires a comment saying why, posted first so it reads above the
127
+ * close event. A refused close leaves the comment posted and the box cleared.
128
+ */
129
+ function CloseWithComment({
130
+ editing,
131
+ draft,
132
+ }: CloseWithCommentProps): ReactNode {
133
+ const { pullRequest } = editing;
134
+ const sendEdit = useSendEdit(editing);
135
+ if (!pullRequest.viewerCanUpdate || pullRequest.state !== "open") return null;
136
+
137
+ return (
138
+ <ActionButton
139
+ text="Close with comment"
140
+ accent="danger"
141
+ variant="tonal"
142
+ size="sm"
143
+ disabledReason={
144
+ draft.body.trim() === ""
145
+ ? "Write a comment saying why it is closed."
146
+ : null
147
+ }
148
+ errorToMessage={errorToMessage}
149
+ onPress={async () => {
150
+ await sendEdit({ kind: "comment", body: draft.body });
151
+ draft.clear();
152
+ await sendEdit({ kind: "state", state: "closed" });
153
+ }}
154
+ />
155
+ );
156
+ }
157
+
158
+ export interface PullRequestTimelineProps {
159
+ detail: PullRequestDetailResponse;
160
+ /** reads the pull request again once GitHub took a change */
161
+ onEdited: (edit: PullRequestEdit) => Promise<void>;
162
+ }
163
+
164
+ /** The description, as GitHub's first comment. */
165
+ export function PullRequestDescription({
166
+ detail,
167
+ onEdited,
168
+ }: PullRequestTimelineProps): ReactNode {
169
+ const { pullRequest } = detail;
170
+ return (
171
+ <CommentCard
172
+ editing={{ pullRequest, onEdited }}
173
+ author={pullRequest.author}
174
+ action="opened"
175
+ createdAt={pullRequest.createdAt}
176
+ lastEditedAt={pullRequest.lastEditedAt}
177
+ url={pullRequest.url}
178
+ body={pullRequest.body}
179
+ emptyText="No description provided."
180
+ canEdit={pullRequest.viewerCanUpdate}
181
+ reactable={pullRequest}
182
+ toEdit={(body) => ({ kind: "body", body })}
183
+ />
184
+ );
185
+ }
186
+
187
+ const isConversation = (item: PullRequestTimelineItem): boolean =>
188
+ item.kind !== "event";
189
+
190
+ /** Comments, reviews and threads only: what a reviewer reads, without the events. */
191
+ export const conversationCount = (
192
+ timeline: PullRequestTimelineItem[],
193
+ ): number => timeline.filter(isConversation).length;
194
+
195
+ export interface PullRequestTimelineEntriesProps extends PullRequestTimelineProps {
196
+ /** leaves out commits, label changes and the other events */
197
+ conversationOnly?: boolean;
198
+ }
199
+
200
+ /** The timeline part, oldest first, once read. */
201
+ export function PullRequestTimelineEntries({
202
+ detail,
203
+ onEdited,
204
+ conversationOnly = false,
205
+ }: PullRequestTimelineEntriesProps): ReactNode {
206
+ const { pullRequest } = detail;
207
+ const { value, error, reading } = usePullRequestDataSource().usePart(
208
+ pullRequest,
209
+ "timeline",
210
+ );
211
+ const editing: ConversationEditing = { pullRequest, onEdited };
212
+ if (value === null) {
213
+ return <PullRequestPartPending error={error} what="the timeline" />;
214
+ }
215
+ const items = conversationOnly
216
+ ? value.timeline.filter(isConversation)
217
+ : value.timeline;
218
+ return (
219
+ <>
220
+ <PullRequestPartReading reading={reading} />
221
+ <PullRequestPartError error={error} />
222
+ {value.timelineTruncated ? (
223
+ <ExternalLinkText
224
+ size="sm"
225
+ href={pullRequest.url}
226
+ text="Older activity is only on GitHub"
227
+ />
228
+ ) : null}
229
+ {groupCommits(items).map((entry, index) => (
230
+ <TimelineEntryView
231
+ key={entryKey(entry, index)}
232
+ editing={editing}
233
+ entry={entry}
234
+ />
235
+ ))}
236
+ </>
237
+ );
238
+ }
239
+
240
+ /**
241
+ * The conversation as GitHub's page has it: the description, then comments,
242
+ * reviews, threads on the code and events oldest first, then a box to comment.
243
+ */
244
+ export function PullRequestTimeline({
245
+ detail,
246
+ onEdited,
247
+ }: PullRequestTimelineProps): ReactNode {
248
+ const { pullRequest } = detail;
249
+ const editing: ConversationEditing = { pullRequest, onEdited };
250
+
251
+ return (
252
+ <View className="gap-m">
253
+ <PullRequestDescription detail={detail} onEdited={onEdited} />
254
+ <PullRequestTimelineEntries detail={detail} onEdited={onEdited} />
255
+ <Composer
256
+ editing={editing}
257
+ draftKey="comment"
258
+ title="Add a comment"
259
+ ariaLabel="New comment"
260
+ buttonText="Comment"
261
+ toEdit={(body) => ({ kind: "comment", body })}
262
+ // a render prop called by the composer, not a component defined here
263
+ // oxlint-disable-next-line react/no-unstable-nested-components
264
+ extraActions={(draft) => (
265
+ <CloseWithComment editing={editing} draft={draft} />
266
+ )}
267
+ />
268
+ </View>
269
+ );
270
+ }
@@ -0,0 +1,42 @@
1
+ import { Text, View, WarningMessage } from "alouette";
2
+ import type { ReactNode } from "react";
3
+ import type { RepositoryAttachment } from "../dataSource.ts";
4
+ import { usePullRequestSlots } from "../slots.tsx";
5
+
6
+ interface RepositoryAttachmentBannerProps {
7
+ ownerRepo: string;
8
+ attachment: RepositoryAttachment;
9
+ /** overrides the `renderRepositoryAction` slot: where the checkout is configured */
10
+ action?: ReactNode;
11
+ }
12
+
13
+ /**
14
+ * Says up front, on every tab, that the diff of this repository cannot be read
15
+ * because no local checkout is attached to it, rather than leaving that to be
16
+ * found in Files changed. An entry that exists but fails its check shows why.
17
+ * Nothing when the checkout is attached.
18
+ */
19
+ export function RepositoryAttachmentBanner({
20
+ ownerRepo,
21
+ attachment,
22
+ action,
23
+ }: RepositoryAttachmentBannerProps): ReactNode {
24
+ const { renderRepositoryAction } = usePullRequestSlots();
25
+ if (attachment.status === "attached") return null;
26
+ const shownAction = action ?? renderRepositoryAction?.(ownerRepo);
27
+ return (
28
+ <WarningMessage>
29
+ <View className="gap-xs">
30
+ <Text className="font-body-bold text-sm">
31
+ {`${ownerRepo} is not attached to a local checkout: the diff cannot be read.`}
32
+ </Text>
33
+ {attachment.status === "invalidPath" ? (
34
+ <Text className="text-sm">{attachment.detail}</Text>
35
+ ) : null}
36
+ {shownAction === null || shownAction === undefined ? null : (
37
+ <View className="flex-row">{shownAction}</View>
38
+ )}
39
+ </View>
40
+ </WarningMessage>
41
+ );
42
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The nearest ancestor that scrolls vertically, which only exists on the web
3
+ * (a DOM element); null when none does. Used to keep a closed file's header in
4
+ * view under the diff toolbar.
5
+ */
6
+ export const scrollParentOf = (element: HTMLElement): HTMLElement | null => {
7
+ for (
8
+ let parent = element.parentElement;
9
+ parent !== null;
10
+ parent = parent.parentElement
11
+ ) {
12
+ const { overflowY } = getComputedStyle(parent);
13
+ if (overflowY === "auto" || overflowY === "scroll") return parent;
14
+ }
15
+ return null;
16
+ };
17
+
18
+ /** A CSS variable as `element` inherits it; "" without a DOM element (native). */
19
+ export const cssVariableOf = (element: unknown, name: string): string =>
20
+ typeof HTMLElement !== "undefined" && element instanceof HTMLElement
21
+ ? getComputedStyle(element).getPropertyValue(name).trim()
22
+ : "";
23
+
24
+ /** How far `element`'s top is above the `stickyTop` pixels a sticky bar covers; negative below. */
25
+ const hiddenAbove = (
26
+ element: HTMLElement,
27
+ container: HTMLElement,
28
+ stickyTop: number,
29
+ ): number =>
30
+ container.getBoundingClientRect().top +
31
+ stickyTop -
32
+ element.getBoundingClientRect().top;
33
+
34
+ /** Scrolls `element`'s top to just under the `stickyTop` pixels a sticky bar covers. */
35
+ export const scrollToTopOf = (
36
+ element: HTMLElement,
37
+ stickyTop: number,
38
+ ): void => {
39
+ const container = scrollParentOf(element);
40
+ if (container === null) return;
41
+ container.scrollTop -= hiddenAbove(element, container, stickyTop);
42
+ };
43
+
44
+ /** As `scrollToTopOf`, only when `element`'s top is hidden above that bar. */
45
+ export const revealTopOf = (element: HTMLElement, stickyTop: number): void => {
46
+ const container = scrollParentOf(element);
47
+ if (container === null) return;
48
+ const hidden = hiddenAbove(element, container, stickyTop);
49
+ if (hidden > 0) container.scrollTop -= hidden;
50
+ };
package/src/css.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module "*.css";
@@ -0,0 +1,242 @@
1
+ import type {
2
+ DiffImpactsResponse,
3
+ DiffReadingOrderResponse,
4
+ DiffRefusal,
5
+ DiffStep,
6
+ DiffWhitespace,
7
+ EditPullRequestResponse,
8
+ PullRequestCandidatesResponse,
9
+ PullRequestDiffResponse,
10
+ PullRequestEdit,
11
+ PullRequestPart,
12
+ PullRequestPartResponse,
13
+ PullRequestRef,
14
+ PullRequestRevisionsResponse,
15
+ PullRequestSummary,
16
+ ReviewflowPrResponse,
17
+ } from "@reviewflow/sdk";
18
+ import { createContext, use } from "react";
19
+
20
+ /**
21
+ * Where a read is at, as the components consume it. Every `use*` hook of the
22
+ * data source answers with this shape.
23
+ *
24
+ * - `value`: the newest answer held, `null` until the first one. It carries its
25
+ * own age (`fetchedAt`, `checkedAt`) for the parts that are
26
+ * `PullRequestRead`s, which is what the screen's "read at" line shows.
27
+ * - `cached`: `value` is an older read shown while the source reads again, not
28
+ * an answer to this request. A source with no cache always says `false`.
29
+ * - `reading`: the source is reading, for the first time or over `value`, and
30
+ * the screen should say so. A source may leave out a re-read over a `value`
31
+ * it holds for up to date, one that only confirms what is shown.
32
+ * - `error`: why the last read failed, as a message; `value` may still hold an
33
+ * older answer.
34
+ */
35
+ export interface PullRequestReadState<T> {
36
+ value: T | null;
37
+ cached: boolean;
38
+ reading: boolean;
39
+ error: string | null;
40
+ }
41
+
42
+ /** Why the source did not read the diff at all; defined by the sdk, which the server answers with. */
43
+ export type PullRequestDiffRefusal = DiffRefusal;
44
+
45
+ /**
46
+ * Whether the source can read a repository's diff from a local checkout, known
47
+ * before any diff is asked for. `detail` says which checkout is used, or why
48
+ * none can be.
49
+ */
50
+ export interface RepositoryAttachment {
51
+ status: "attached" | "invalidPath" | "notConfigured";
52
+ detail: string;
53
+ }
54
+
55
+ /** The range a diff is read over: two shas, which name it for good. */
56
+ export interface PullRequestDiffRange {
57
+ head: string;
58
+ base: string;
59
+ whitespace: DiffWhitespace;
60
+ }
61
+
62
+ export interface PullRequestDiffState extends PullRequestReadState<PullRequestDiffResponse> {
63
+ /**
64
+ * What the source is waiting on while `reading`: `fetching` the commits into
65
+ * the checkout, `diffing` them. `null` when it does not know, or is not
66
+ * reading. A large fetch takes tens of seconds, which a bare spinner hides.
67
+ */
68
+ step: DiffStep | null;
69
+ /** set instead of `value` when the source refused to read, see {@link PullRequestDiffRefusal} */
70
+ refused: PullRequestDiffRefusal | null;
71
+ }
72
+
73
+ export interface PullRequestDiffsState extends PullRequestReadState<
74
+ PullRequestDiffResponse[]
75
+ > {
76
+ /** the first range the source refused to read, as for the diff */
77
+ refused: PullRequestDiffRefusal | null;
78
+ }
79
+
80
+ export interface PullRequestReadingOrderState extends PullRequestReadState<DiffReadingOrderResponse> {
81
+ /** set instead of `value` when the source refused to read, as for the diff */
82
+ refused: PullRequestDiffRefusal | null;
83
+ }
84
+
85
+ export interface PullRequestImpactsState extends PullRequestReadState<DiffImpactsResponse> {
86
+ /** set instead of `value` when the source refused to read, as for the diff */
87
+ refused: PullRequestDiffRefusal | null;
88
+ }
89
+
90
+ /**
91
+ * What the pull request screen reads and writes through. The app provides one
92
+ * with {@link PullRequestDataSourceProvider}; the components never reach for a
93
+ * network or a cache themselves.
94
+ *
95
+ * Hooks follow the rules of hooks (called unconditionally, in the same order);
96
+ * `ref` and the other arguments may change between renders, and the state then
97
+ * follows them.
98
+ */
99
+ export interface PullRequestDataSource {
100
+ /**
101
+ * What the app already holds of the pull request (a copy from another
102
+ * source, reviewflow's say), enough to head the screen until the `detail`
103
+ * part arrives. Optional: without it the screen shows its pending notice
104
+ * alone. `null` when the app holds nothing for this pull request.
105
+ */
106
+ useSummary?: (ref: PullRequestRef) => PullRequestSummary | null;
107
+
108
+ /**
109
+ * One part of the pull request (`detail`, `checks`, `timeline`, `commits`,
110
+ * `files`). Read when a component showing it mounts, never on a timer.
111
+ */
112
+ usePart: <Part extends PullRequestPart>(
113
+ ref: PullRequestRef,
114
+ part: Part,
115
+ ) => PullRequestReadState<PullRequestPartResponse<Part>>;
116
+
117
+ /**
118
+ * Whether the repository has a local checkout the diff can be read from, so
119
+ * the screen can say so on every tab rather than only once a diff is asked
120
+ * for. `null` while unknown: no banner. Optional: a source that always has
121
+ * its checkouts (or reads no diff from one) leaves it out.
122
+ */
123
+ useRepositoryAttachment?: (
124
+ ref: PullRequestRef,
125
+ ) => RepositoryAttachment | null;
126
+
127
+ /**
128
+ * reviewflow's copy of the pull request: its checks are judged by its rules
129
+ * (checks allowed to fail, jobs on hold, its own check left out). Optional: a
130
+ * source without reviewflow leaves it out, and nothing is shown in its place.
131
+ */
132
+ useReviewflowPr?: (
133
+ ref: PullRequestRef,
134
+ ) => PullRequestReadState<ReviewflowPrResponse>;
135
+
136
+ /** What the reviewer, assignee and label editors offer. Not read again by `reload`. */
137
+ useCandidates: (
138
+ ref: PullRequestRef,
139
+ ) => PullRequestReadState<PullRequestCandidatesResponse>;
140
+
141
+ /** The edit history of a description, a comment or a review (`subjectId` is its GitHub node id). */
142
+ useRevisions: (
143
+ ref: PullRequestRef,
144
+ subjectId: string,
145
+ ) => PullRequestReadState<PullRequestRevisionsResponse>;
146
+
147
+ /**
148
+ * The pull request's changes over `range`. `null` reads nothing (a
149
+ * component that may need a diff later). While a new range loads, the
150
+ * previous `value` should stay (`cached: true`) so the viewer does not flash.
151
+ */
152
+ useDiff: (
153
+ ref: PullRequestRef,
154
+ range: PullRequestDiffRange | null,
155
+ ) => PullRequestDiffState;
156
+
157
+ /**
158
+ * Several ranges' changes, each read and kept as `useDiff` reads it, so a
159
+ * range read here is not read again there. `value` is every answer in the
160
+ * order of `ranges`, `null` until the last one arrived. `null` reads
161
+ * nothing.
162
+ */
163
+ useDiffs: (
164
+ ref: PullRequestRef,
165
+ ranges: readonly PullRequestDiffRange[] | null,
166
+ ) => PullRequestDiffsState;
167
+
168
+ /**
169
+ * The files of the range's diff in reading order (experimental). `null`
170
+ * reads nothing: the reader has not asked for that order. Optional: without
171
+ * it the diff offers only its order by package.
172
+ */
173
+ useReadingOrder?: (
174
+ ref: PullRequestRef,
175
+ range: { head: string; base: string } | null,
176
+ ) => PullRequestReadingOrderState;
177
+
178
+ /**
179
+ * The exports the range's change touches and the files outside the diff that
180
+ * use them (experimental). `null` reads nothing. Optional: without it the
181
+ * diff shows no impacts.
182
+ */
183
+ useImpacts?: (
184
+ ref: PullRequestRef,
185
+ range: { head: string; base: string } | null,
186
+ ) => PullRequestImpactsState;
187
+
188
+ /**
189
+ * One file at a commit, so the diff can unfold the lines the patch left out.
190
+ * A promise rather than a hook: it is asked from a press, long after
191
+ * render. Rejects with a message when the file cannot be read.
192
+ */
193
+ readFileAtSha: (
194
+ ref: PullRequestRef,
195
+ sha: string,
196
+ path: string,
197
+ ) => Promise<string>;
198
+
199
+ /**
200
+ * Writes to GitHub, resolving with the server's answer once GitHub took the
201
+ * change (`followUpError` is what failed after the write landed) and
202
+ * rejecting with the reason when it did not. Does not re-read: the screen calls `reload`
203
+ * with the parts `pullRequestPartsStaleAfter` names. A `viewed` edit stales
204
+ * nothing, so the source applies it to the `files` it holds
205
+ * (`withFileViewed`) before resolving.
206
+ */
207
+ edit: (
208
+ ref: PullRequestRef,
209
+ edit: PullRequestEdit,
210
+ ) => Promise<EditPullRequestResponse>;
211
+
212
+ /**
213
+ * Reads `parts` again from GitHub, every part when absent, and resolves once
214
+ * the parts a mounted component shows are read. A failure is held by the
215
+ * state of the hook (`error`), so this never rejects.
216
+ */
217
+ reload: (
218
+ ref: PullRequestRef,
219
+ parts?: readonly PullRequestPart[],
220
+ ) => Promise<void>;
221
+ }
222
+
223
+ const DataSourceContext = createContext<PullRequestDataSource | null>(null);
224
+
225
+ /** Provides the data source to every pull request component below it. */
226
+ export const PullRequestDataSourceProvider = DataSourceContext.Provider;
227
+
228
+ export const usePullRequestDataSource = (): PullRequestDataSource => {
229
+ const source = use(DataSourceContext);
230
+ if (source === null) {
231
+ throw new Error(
232
+ "usePullRequestDataSource needs a PullRequestDataSourceProvider",
233
+ );
234
+ }
235
+ return source;
236
+ };
237
+
238
+ /** The shas a diff and a review are pinned to. */
239
+ export interface PullRequestShas {
240
+ head: string;
241
+ base: string;
242
+ }