@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,143 @@
|
|
|
1
|
+
import type { PullRequestCommit } from "@reviewflow/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How a file of one commit's diff relates to the pull request's diff, which
|
|
5
|
+
* threads, pending comments and viewed marks are read on.
|
|
6
|
+
*
|
|
7
|
+
* - `pullRequest`: no other commit of the pull request changes it, so both
|
|
8
|
+
* sides of this commit's diff are the pull request's.
|
|
9
|
+
* - `head`: an earlier commit changed it too: the new side is the head's, the
|
|
10
|
+
* old side is not the base's.
|
|
11
|
+
* - `commit`: a later commit changes it again: neither side is the pull
|
|
12
|
+
* request's, so a comment is anchored on this commit and goes out alone.
|
|
13
|
+
*/
|
|
14
|
+
export type CommitFileAnchoring = "commit" | "head" | "pullRequest";
|
|
15
|
+
|
|
16
|
+
export type CommitsToRead =
|
|
17
|
+
| { kind: "available"; commits: PullRequestCommit[] }
|
|
18
|
+
| { kind: "none" }
|
|
19
|
+
| { kind: "unavailable"; reason: string };
|
|
20
|
+
|
|
21
|
+
interface CommitsToReadParams {
|
|
22
|
+
/** oldest first, as GitHub lists them */
|
|
23
|
+
listed: PullRequestCommit[];
|
|
24
|
+
/** every commit of the pull request, `listed` included */
|
|
25
|
+
total: number;
|
|
26
|
+
/** the head the diff shows */
|
|
27
|
+
head: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isChain = (commits: PullRequestCommit[]): boolean =>
|
|
31
|
+
commits.every(
|
|
32
|
+
(commit, index) =>
|
|
33
|
+
index === 0 || commit.parentOid === commits[index - 1]?.oid,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The commits up to the head shown, which can be read one by one only when
|
|
38
|
+
* each one's diff against its parent is a part of the pull request's: a chain
|
|
39
|
+
* with no merge, which a merge would break by carrying what it merged in.
|
|
40
|
+
*/
|
|
41
|
+
export const commitsToRead = ({
|
|
42
|
+
listed,
|
|
43
|
+
total,
|
|
44
|
+
head,
|
|
45
|
+
}: CommitsToReadParams): CommitsToRead => {
|
|
46
|
+
const end = listed.findIndex(({ oid }) => oid === head);
|
|
47
|
+
if (end === -1) {
|
|
48
|
+
return {
|
|
49
|
+
kind: "unavailable",
|
|
50
|
+
reason:
|
|
51
|
+
total > listed.length
|
|
52
|
+
? "The pull request has more commits than GitHub lists at once"
|
|
53
|
+
: "The commits listed do not lead to the head shown",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const commits = listed.slice(0, end + 1);
|
|
57
|
+
if (commits.length < 2) return { kind: "none" };
|
|
58
|
+
if (commits.some(({ merge }) => merge)) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "unavailable",
|
|
61
|
+
reason: "A merge commit's own diff carries what it merged in",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (commits[0]?.parentOid === null || !isChain(commits)) {
|
|
65
|
+
return {
|
|
66
|
+
kind: "unavailable",
|
|
67
|
+
reason: "The commits do not follow one another",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { kind: "available", commits };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** The index of the commit after `oid`; 0, every commit, when `oid` is null or not one of them. */
|
|
74
|
+
export const firstCommitAfter = (
|
|
75
|
+
commits: PullRequestCommit[],
|
|
76
|
+
oid: string | null,
|
|
77
|
+
): number =>
|
|
78
|
+
oid === null ? 0 : commits.findIndex((commit) => commit.oid === oid) + 1;
|
|
79
|
+
|
|
80
|
+
/** No path is changed by more than one commit. */
|
|
81
|
+
export const touchDifferentFiles = (
|
|
82
|
+
pathsByCommit: readonly ReadonlySet<string>[],
|
|
83
|
+
): boolean => {
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
for (const paths of pathsByCommit) {
|
|
86
|
+
for (const path of paths) {
|
|
87
|
+
if (seen.has(path)) return false;
|
|
88
|
+
seen.add(path);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const unionOf = (pathsByCommit: readonly ReadonlySet<string>[]): Set<string> =>
|
|
95
|
+
new Set(pathsByCommit.flatMap((paths) => [...paths]));
|
|
96
|
+
|
|
97
|
+
/** Every path of the commit at `index`, with how it is anchored. */
|
|
98
|
+
export const commitAnchorings = (
|
|
99
|
+
pathsByCommit: readonly ReadonlySet<string>[],
|
|
100
|
+
index: number,
|
|
101
|
+
): Map<string, CommitFileAnchoring> => {
|
|
102
|
+
const earlier = unionOf(pathsByCommit.slice(0, index));
|
|
103
|
+
const later = unionOf(pathsByCommit.slice(index + 1));
|
|
104
|
+
const anchoringOf = (path: string): CommitFileAnchoring => {
|
|
105
|
+
if (later.has(path)) return "commit";
|
|
106
|
+
if (earlier.has(path)) return "head";
|
|
107
|
+
return "pullRequest";
|
|
108
|
+
};
|
|
109
|
+
return new Map(
|
|
110
|
+
[...(pathsByCommit[index] ?? [])].map((path) => [path, anchoringOf(path)]),
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** What one commit changes, to pick it from a list. */
|
|
115
|
+
export interface CommitSummary {
|
|
116
|
+
/** the commit's workspaces, the root named "Root" */
|
|
117
|
+
workspaces: string[];
|
|
118
|
+
/** the files it changes, as `changedPathsOf` counts them */
|
|
119
|
+
files: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface SummarizeCommitsParams {
|
|
123
|
+
/** the diff of each readable commit, as loaded */
|
|
124
|
+
diffs: readonly { groups: readonly { name: string }[] }[];
|
|
125
|
+
/** the paths each diff changes, aligned with `diffs` */
|
|
126
|
+
pathsByCommit: readonly ReadonlySet<string>[];
|
|
127
|
+
/** where the stepped commits start in the readable ones */
|
|
128
|
+
sinceIndex: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The summaries of the stepped commits: the diffs are those of every readable
|
|
133
|
+
* commit, so those before `sinceIndex` are left out, as the commits are.
|
|
134
|
+
*/
|
|
135
|
+
export const summarizeCommits = ({
|
|
136
|
+
diffs,
|
|
137
|
+
pathsByCommit,
|
|
138
|
+
sinceIndex,
|
|
139
|
+
}: SummarizeCommitsParams): CommitSummary[] =>
|
|
140
|
+
diffs.slice(sinceIndex).map(({ groups }, index) => ({
|
|
141
|
+
workspaces: groups.map(({ name }) => (name === "" ? "Root" : name)),
|
|
142
|
+
files: pathsByCommit[sinceIndex + index]?.size ?? 0,
|
|
143
|
+
}));
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PullRequestCommit,
|
|
3
|
+
PullRequestDetailResponse,
|
|
4
|
+
PullRequestEdit,
|
|
5
|
+
} from "@reviewflow/sdk";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Code,
|
|
9
|
+
InfoMessage,
|
|
10
|
+
PressableListItem,
|
|
11
|
+
Text,
|
|
12
|
+
View,
|
|
13
|
+
} from "alouette";
|
|
14
|
+
import { ArrowLeftRegularIcon } from "alouette-icons/phosphor-icons/ArrowLeft";
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { usePullRequestDataSource } from "../dataSource.ts";
|
|
18
|
+
import { formatDataAge } from "../services/formatDataAge.ts";
|
|
19
|
+
import { PersonAvatar } from "./pull-request-conversation-person.tsx";
|
|
20
|
+
import { PullRequestDiffView } from "./pull-request-diff-view.tsx";
|
|
21
|
+
import {
|
|
22
|
+
PullRequestPartError,
|
|
23
|
+
PullRequestPartPending,
|
|
24
|
+
PullRequestPartReading,
|
|
25
|
+
} from "./pull-request-part-notice.tsx";
|
|
26
|
+
|
|
27
|
+
export interface PullRequestCommitsProps {
|
|
28
|
+
detail: PullRequestDetailResponse;
|
|
29
|
+
/** reads the pull request again once GitHub took a comment */
|
|
30
|
+
onEdited: (edit: PullRequestEdit) => Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* The commit whose diff is open, by oid; absent: the component holds it
|
|
33
|
+
* itself. Pass it, with `onOpenCommitChange`, to keep it in a url.
|
|
34
|
+
*/
|
|
35
|
+
openCommit?: string | null;
|
|
36
|
+
onOpenCommitChange?: (oid: string | null) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const shortSha = (oid: string): string => oid.slice(0, 7);
|
|
40
|
+
|
|
41
|
+
const dayOf = (commit: PullRequestCommit): string =>
|
|
42
|
+
new Date(commit.committedAt).toLocaleDateString([], {
|
|
43
|
+
year: "numeric",
|
|
44
|
+
month: "long",
|
|
45
|
+
day: "numeric",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
interface CommitRowProps {
|
|
49
|
+
commit: PullRequestCommit;
|
|
50
|
+
onOpen: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A link, as a board row is: it opens the commit's own page, which a url keeps. */
|
|
54
|
+
function CommitRow({ commit, onOpen }: CommitRowProps): ReactNode {
|
|
55
|
+
const login = commit.author?.login ?? "unknown";
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<PressableListItem
|
|
59
|
+
role="link"
|
|
60
|
+
aria-label={`Show the changes of ${commit.headline}`}
|
|
61
|
+
onPress={onOpen}
|
|
62
|
+
>
|
|
63
|
+
<View className="flex-row items-center gap-sm">
|
|
64
|
+
<PersonAvatar person={commit.author} />
|
|
65
|
+
<View className="min-w-0 flex-1">
|
|
66
|
+
<Text className="text-sm font-body-bold" numberOfLines={1}>
|
|
67
|
+
{commit.headline}
|
|
68
|
+
</Text>
|
|
69
|
+
<Text className="text-xs text-muted" numberOfLines={1}>
|
|
70
|
+
{`${login} committed ${formatDataAge(commit.committedAt)}`}
|
|
71
|
+
</Text>
|
|
72
|
+
</View>
|
|
73
|
+
<Code className="text-xs">{shortSha(commit.oid)}</Code>
|
|
74
|
+
</View>
|
|
75
|
+
</PressableListItem>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface CommitDiffProps {
|
|
80
|
+
detail: PullRequestDetailResponse;
|
|
81
|
+
commit: PullRequestCommit;
|
|
82
|
+
onBack: () => void;
|
|
83
|
+
onEdited: (edit: PullRequestEdit) => Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A commit's own diff. A comment is anchored on the commit, as GitHub's
|
|
88
|
+
* commit view does, on the new side only: GitHub reads the old side's line
|
|
89
|
+
* numbers against the pull request's base, not this commit's parent.
|
|
90
|
+
*/
|
|
91
|
+
function CommitDiff({
|
|
92
|
+
detail,
|
|
93
|
+
commit,
|
|
94
|
+
onBack,
|
|
95
|
+
onEdited,
|
|
96
|
+
}: CommitDiffProps): ReactNode {
|
|
97
|
+
const { pullRequest } = detail;
|
|
98
|
+
const dataSource = usePullRequestDataSource();
|
|
99
|
+
return (
|
|
100
|
+
<View className="gap-m">
|
|
101
|
+
<View className="flex-row flex-wrap items-center gap-sm">
|
|
102
|
+
<Button
|
|
103
|
+
text="Back to commits"
|
|
104
|
+
icon={<ArrowLeftRegularIcon />}
|
|
105
|
+
accent="neutral"
|
|
106
|
+
variant="tonal"
|
|
107
|
+
size="sm"
|
|
108
|
+
onPress={onBack}
|
|
109
|
+
/>
|
|
110
|
+
<Text className="min-w-0 flex-1 text-sm font-body-bold">
|
|
111
|
+
{commit.headline}
|
|
112
|
+
</Text>
|
|
113
|
+
<Code className="text-xs">{shortSha(commit.oid)}</Code>
|
|
114
|
+
</View>
|
|
115
|
+
{commit.body.trim() === "" ? null : (
|
|
116
|
+
<Text className="select-auto font-mono text-xs text-muted">
|
|
117
|
+
{commit.body}
|
|
118
|
+
</Text>
|
|
119
|
+
)}
|
|
120
|
+
{commit.parentOid === null ? (
|
|
121
|
+
<InfoMessage>
|
|
122
|
+
This is a root commit with no parent: its changes cannot be shown.
|
|
123
|
+
</InfoMessage>
|
|
124
|
+
) : (
|
|
125
|
+
<PullRequestDiffView
|
|
126
|
+
range={{
|
|
127
|
+
ownerRepo: pullRequest.ownerRepo,
|
|
128
|
+
number: pullRequest.number,
|
|
129
|
+
head: commit.oid,
|
|
130
|
+
base: commit.parentOid,
|
|
131
|
+
}}
|
|
132
|
+
review={{
|
|
133
|
+
commentable: true,
|
|
134
|
+
rightOnly: true,
|
|
135
|
+
commitOid: commit.oid,
|
|
136
|
+
headSha: pullRequest.headSha,
|
|
137
|
+
threads: [],
|
|
138
|
+
files: [],
|
|
139
|
+
onEdit: async (edit) => {
|
|
140
|
+
await dataSource.edit(pullRequest, edit);
|
|
141
|
+
await onEdited(edit);
|
|
142
|
+
},
|
|
143
|
+
onViewed: () => Promise.resolve(),
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
</View>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Oldest first, grouped by day as on GitHub; pressing a commit shows its own diff, read-only. */
|
|
152
|
+
export function PullRequestCommits({
|
|
153
|
+
detail,
|
|
154
|
+
onEdited,
|
|
155
|
+
openCommit,
|
|
156
|
+
onOpenCommitChange,
|
|
157
|
+
}: PullRequestCommitsProps): ReactNode {
|
|
158
|
+
const { pullRequest } = detail;
|
|
159
|
+
const { usePart } = usePullRequestDataSource();
|
|
160
|
+
const [localOid, setLocalOid] = useState<string | null>(null);
|
|
161
|
+
const openOid = openCommit === undefined ? localOid : openCommit;
|
|
162
|
+
const { value, error, reading } = usePart(pullRequest, "commits");
|
|
163
|
+
if (value === null) {
|
|
164
|
+
return <PullRequestPartPending error={error} what="the commits" />;
|
|
165
|
+
}
|
|
166
|
+
const { commits: listed } = value;
|
|
167
|
+
const opened = listed.find(({ oid }) => oid === openOid);
|
|
168
|
+
const setOpenOid = (oid: string | null): void => {
|
|
169
|
+
setLocalOid(oid);
|
|
170
|
+
onOpenCommitChange?.(oid);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (opened !== undefined) {
|
|
174
|
+
return (
|
|
175
|
+
<CommitDiff
|
|
176
|
+
key={opened.oid}
|
|
177
|
+
detail={detail}
|
|
178
|
+
commit={opened}
|
|
179
|
+
onEdited={onEdited}
|
|
180
|
+
onBack={() => {
|
|
181
|
+
setOpenOid(null);
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const days = new Map<string, PullRequestCommit[]>();
|
|
188
|
+
for (const commit of listed) {
|
|
189
|
+
const day = dayOf(commit);
|
|
190
|
+
const ofDay = days.get(day);
|
|
191
|
+
if (ofDay === undefined) days.set(day, [commit]);
|
|
192
|
+
else ofDay.push(commit);
|
|
193
|
+
}
|
|
194
|
+
const unlisted = pullRequest.commitsTotal - listed.length;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<View className="gap-m">
|
|
198
|
+
<PullRequestPartReading reading={reading} />
|
|
199
|
+
<PullRequestPartError error={error} />
|
|
200
|
+
{[...days].map(([day, commits]) => (
|
|
201
|
+
<View key={day} className="gap-xs">
|
|
202
|
+
<Text className="text-xs font-body-bold text-muted">{day}</Text>
|
|
203
|
+
{commits.map((commit) => (
|
|
204
|
+
<CommitRow
|
|
205
|
+
key={commit.oid}
|
|
206
|
+
commit={commit}
|
|
207
|
+
onOpen={() => {
|
|
208
|
+
setOpenOid(commit.oid);
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
))}
|
|
212
|
+
</View>
|
|
213
|
+
))}
|
|
214
|
+
{unlisted > 0 ? (
|
|
215
|
+
<Text className="text-xs text-muted">
|
|
216
|
+
{`${String(unlisted)} more commit${unlisted === 1 ? "" : "s"} not listed`}
|
|
217
|
+
</Text>
|
|
218
|
+
) : null}
|
|
219
|
+
</View>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PullRequestEdit,
|
|
3
|
+
PullRequestPerson,
|
|
4
|
+
PullRequestReactable,
|
|
5
|
+
PullRequestReviewState,
|
|
6
|
+
} from "@reviewflow/sdk";
|
|
7
|
+
import type { Accent } from "alouette";
|
|
8
|
+
import {
|
|
9
|
+
Badge,
|
|
10
|
+
Box,
|
|
11
|
+
ExternalLinkText,
|
|
12
|
+
IconButton,
|
|
13
|
+
Text,
|
|
14
|
+
Tooltip,
|
|
15
|
+
View,
|
|
16
|
+
} from "alouette";
|
|
17
|
+
import { PencilSimpleRegularIcon } from "alouette-icons/phosphor-icons/PencilSimple";
|
|
18
|
+
import type { ReactNode } from "react";
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { useHasDraftText } from "../services/PullRequestDraftsProvider.tsx";
|
|
21
|
+
import { formatDataAge } from "../services/formatDataAge.ts";
|
|
22
|
+
import { linkInRow } from "./link-in-row.ts";
|
|
23
|
+
import { MarkdownText } from "./markdown-text.tsx";
|
|
24
|
+
import type { ConversationEditing } from "./pull-request-conversation-edit.tsx";
|
|
25
|
+
import { BodyEditor, useSendEdit } from "./pull-request-conversation-edit.tsx";
|
|
26
|
+
import { PersonAvatar } from "./pull-request-conversation-person.tsx";
|
|
27
|
+
import { PullRequestReactions } from "./pull-request-reactions.tsx";
|
|
28
|
+
import { EditedLink } from "./pull-request-revisions.tsx";
|
|
29
|
+
|
|
30
|
+
const verdictLabel = (state: PullRequestReviewState): string => {
|
|
31
|
+
if (state === "approved") return "approved these changes";
|
|
32
|
+
if (state === "changesRequested") return "requested changes";
|
|
33
|
+
if (state === "dismissed") return "review dismissed";
|
|
34
|
+
if (state === "pending") return "pending review";
|
|
35
|
+
return "reviewed";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const verdictAccent = (state: PullRequestReviewState): Accent => {
|
|
39
|
+
if (state === "approved") return "success";
|
|
40
|
+
if (state === "changesRequested") return "danger";
|
|
41
|
+
if (state === "pending") return "warning";
|
|
42
|
+
return "info";
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
interface CardHeaderProps {
|
|
46
|
+
author: PullRequestPerson | null;
|
|
47
|
+
/** a review's verdict, shown as a badge instead of `action` */
|
|
48
|
+
verdict?: PullRequestReviewState;
|
|
49
|
+
action: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
url: string;
|
|
52
|
+
edited: ReactNode;
|
|
53
|
+
onEdit: (() => void) | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function CardHeader({
|
|
57
|
+
author,
|
|
58
|
+
verdict,
|
|
59
|
+
action,
|
|
60
|
+
createdAt,
|
|
61
|
+
url,
|
|
62
|
+
edited,
|
|
63
|
+
onEdit,
|
|
64
|
+
}: CardHeaderProps): ReactNode {
|
|
65
|
+
return (
|
|
66
|
+
<View className="flex-row items-center gap-xs">
|
|
67
|
+
<View className="min-w-0 flex-1 flex-row flex-wrap items-center gap-xs">
|
|
68
|
+
<PersonAvatar person={author} />
|
|
69
|
+
<Text className="text-sm font-body-bold">
|
|
70
|
+
{author?.login ?? "ghost"}
|
|
71
|
+
</Text>
|
|
72
|
+
{verdict === undefined ? (
|
|
73
|
+
<Text className="text-sm text-muted">{action}</Text>
|
|
74
|
+
) : (
|
|
75
|
+
<Badge accent={verdictAccent(verdict)} size="sm">
|
|
76
|
+
{verdictLabel(verdict)}
|
|
77
|
+
</Badge>
|
|
78
|
+
)}
|
|
79
|
+
<ExternalLinkText
|
|
80
|
+
size="sm"
|
|
81
|
+
href={url}
|
|
82
|
+
text={formatDataAge(createdAt)}
|
|
83
|
+
className={linkInRow}
|
|
84
|
+
/>
|
|
85
|
+
{edited}
|
|
86
|
+
</View>
|
|
87
|
+
{onEdit === null ? null : (
|
|
88
|
+
<Tooltip
|
|
89
|
+
content="Edit the text, on GitHub too"
|
|
90
|
+
render={(triggerProps) => (
|
|
91
|
+
<IconButton
|
|
92
|
+
{...triggerProps}
|
|
93
|
+
icon={<PencilSimpleRegularIcon />}
|
|
94
|
+
aria-label="Edit"
|
|
95
|
+
accent="neutral"
|
|
96
|
+
variant="soft"
|
|
97
|
+
size="sm"
|
|
98
|
+
onPress={onEdit}
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
</View>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface CommentCardProps {
|
|
108
|
+
editing: ConversationEditing;
|
|
109
|
+
author: PullRequestPerson | null;
|
|
110
|
+
action: string;
|
|
111
|
+
verdict?: PullRequestReviewState;
|
|
112
|
+
createdAt: string;
|
|
113
|
+
lastEditedAt: string | null;
|
|
114
|
+
url: string;
|
|
115
|
+
body: string;
|
|
116
|
+
/** what an empty body says; without it an empty body renders nothing */
|
|
117
|
+
emptyText?: string;
|
|
118
|
+
canEdit: boolean;
|
|
119
|
+
toEdit: (body: string) => PullRequestEdit;
|
|
120
|
+
/** what the reactions under the body are those of */
|
|
121
|
+
reactable: PullRequestReactable;
|
|
122
|
+
/** inside a thread's frame, which is already the card */
|
|
123
|
+
bare?: boolean;
|
|
124
|
+
/** under the body, hidden with it while editing */
|
|
125
|
+
footer?: ReactNode;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* One piece of the conversation with its author: the body is rendered
|
|
130
|
+
* markdown, edited in place through `toEdit`, and its task checkboxes save the
|
|
131
|
+
* edited source the same way.
|
|
132
|
+
*/
|
|
133
|
+
export function CommentCard({
|
|
134
|
+
editing,
|
|
135
|
+
author,
|
|
136
|
+
action,
|
|
137
|
+
verdict,
|
|
138
|
+
createdAt,
|
|
139
|
+
lastEditedAt,
|
|
140
|
+
url,
|
|
141
|
+
body,
|
|
142
|
+
emptyText,
|
|
143
|
+
canEdit,
|
|
144
|
+
toEdit,
|
|
145
|
+
reactable,
|
|
146
|
+
bare = false,
|
|
147
|
+
footer,
|
|
148
|
+
}: CommentCardProps): ReactNode {
|
|
149
|
+
const sendEdit = useSendEdit(editing);
|
|
150
|
+
const draftKey = `edit:${reactable.id}`;
|
|
151
|
+
// an edit left unsent on an earlier visit opens again on its draft
|
|
152
|
+
const hasDraft = useHasDraftText(draftKey);
|
|
153
|
+
const [editorOpen, setEditorOpen] = useState(hasDraft);
|
|
154
|
+
const empty = body.trim() === "";
|
|
155
|
+
|
|
156
|
+
const content = (
|
|
157
|
+
<>
|
|
158
|
+
<CardHeader
|
|
159
|
+
author={author}
|
|
160
|
+
verdict={verdict}
|
|
161
|
+
action={action}
|
|
162
|
+
createdAt={createdAt}
|
|
163
|
+
url={url}
|
|
164
|
+
edited={
|
|
165
|
+
<EditedLink
|
|
166
|
+
pullRequest={editing.pullRequest}
|
|
167
|
+
subjectId={reactable.id}
|
|
168
|
+
lastEditedAt={lastEditedAt}
|
|
169
|
+
url={url}
|
|
170
|
+
/>
|
|
171
|
+
}
|
|
172
|
+
onEdit={
|
|
173
|
+
canEdit && !editorOpen
|
|
174
|
+
? () => {
|
|
175
|
+
setEditorOpen(true);
|
|
176
|
+
}
|
|
177
|
+
: null
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
{editorOpen ? (
|
|
181
|
+
<BodyEditor
|
|
182
|
+
editing={editing}
|
|
183
|
+
draftKey={draftKey}
|
|
184
|
+
initial={body}
|
|
185
|
+
ariaLabel="Edit"
|
|
186
|
+
toEdit={toEdit}
|
|
187
|
+
onDone={() => {
|
|
188
|
+
setEditorOpen(false);
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
191
|
+
) : null}
|
|
192
|
+
{!editorOpen && !empty ? (
|
|
193
|
+
<MarkdownText
|
|
194
|
+
github
|
|
195
|
+
text={body}
|
|
196
|
+
onTaskToggle={
|
|
197
|
+
canEdit
|
|
198
|
+
? async (nextText) => {
|
|
199
|
+
await sendEdit(toEdit(nextText));
|
|
200
|
+
}
|
|
201
|
+
: undefined
|
|
202
|
+
}
|
|
203
|
+
/>
|
|
204
|
+
) : null}
|
|
205
|
+
{!editorOpen && empty && emptyText !== undefined ? (
|
|
206
|
+
<Text className="text-sm text-muted">{emptyText}</Text>
|
|
207
|
+
) : null}
|
|
208
|
+
{editorOpen ? null : footer}
|
|
209
|
+
{editorOpen ? null : (
|
|
210
|
+
<PullRequestReactions
|
|
211
|
+
subject={reactable}
|
|
212
|
+
onEdit={(edit) => sendEdit(edit)}
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
</>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (bare) return <View className="min-w-0 gap-xs">{content}</View>;
|
|
219
|
+
return <Box className="surface min-w-0 gap-sm p-m">{content}</Box>;
|
|
220
|
+
}
|