@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,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
|
+
}
|