@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,416 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DiffGroup,
|
|
3
|
+
DiffImpactsResponse,
|
|
4
|
+
DiffOmittedFile,
|
|
5
|
+
DiffReadingOrderResponse,
|
|
6
|
+
} from "@reviewflow/sdk";
|
|
7
|
+
import { isAttachablePath, subjectsOf } from "@reviewflow/sdk";
|
|
8
|
+
import { Icon, PressableBox, Text, View } from "alouette";
|
|
9
|
+
import { FileRegularIcon } from "alouette-icons/phosphor-icons/File";
|
|
10
|
+
import { FileTsRegularIcon } from "alouette-icons/phosphor-icons/FileTs";
|
|
11
|
+
import { PackageDuotoneIcon } from "alouette-icons/phosphor-icons/Package";
|
|
12
|
+
import { TestTubeRegularIcon } from "alouette-icons/phosphor-icons/TestTube";
|
|
13
|
+
import type { ReactNode } from "react";
|
|
14
|
+
import { PullRequestDiffImpactsSummary } from "./pull-request-diff-impacts-summary.tsx";
|
|
15
|
+
import type { ImpactsStatus } from "./pull-request-diff-impacts.ts";
|
|
16
|
+
import { pluralOf } from "./pull-request-diff-impacts.ts";
|
|
17
|
+
|
|
18
|
+
/** Past this many names over the range, the rows carry their counts only. */
|
|
19
|
+
export const maxOverviewNames = 50;
|
|
20
|
+
|
|
21
|
+
/** `none`: no checkout, or a source that cannot order. */
|
|
22
|
+
export type OverviewReadingOrderStatus =
|
|
23
|
+
| "failed"
|
|
24
|
+
| "none"
|
|
25
|
+
| "reading"
|
|
26
|
+
| "ready";
|
|
27
|
+
|
|
28
|
+
interface OverviewName {
|
|
29
|
+
path: string;
|
|
30
|
+
entry: boolean;
|
|
31
|
+
/** in the patch, so the viewer renders it: false for an omitted file */
|
|
32
|
+
shown: boolean;
|
|
33
|
+
/** the file a test covers, listed above it; null for any other name */
|
|
34
|
+
subject: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface OverviewWorkspace {
|
|
38
|
+
name: string;
|
|
39
|
+
files: number;
|
|
40
|
+
tests: number;
|
|
41
|
+
names: OverviewName[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The workspace of a path the groups do not list (an omitted file): the
|
|
46
|
+
* longest group directory holding it, else its first directory, as the diff
|
|
47
|
+
* groups a repository without checkout.
|
|
48
|
+
*/
|
|
49
|
+
const workspaceOf = (path: string, names: readonly string[]): string => {
|
|
50
|
+
const holding = names
|
|
51
|
+
.filter((name) => name !== "" && path.startsWith(`${name}/`))
|
|
52
|
+
.toSorted((left, right) => right.length - left.length)[0];
|
|
53
|
+
if (holding !== undefined) return holding;
|
|
54
|
+
if (names.includes("")) return "";
|
|
55
|
+
const slash = path.indexOf("/");
|
|
56
|
+
return slash === -1 ? "" : path.slice(0, slash);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
interface SummarizeInput {
|
|
60
|
+
groups: readonly DiffGroup[];
|
|
61
|
+
omitted: readonly DiffOmittedFile[];
|
|
62
|
+
readingOrder: DiffReadingOrderResponse | null;
|
|
63
|
+
displayOrder: DiffReadingOrderResponse | null;
|
|
64
|
+
/** every file rather than the roots */
|
|
65
|
+
every: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The paths as the viewer lays them out: by the reading order when it orders
|
|
70
|
+
* the files, its tests below each file; by the groups otherwise. A path it
|
|
71
|
+
* does not name comes after, as the viewer appends it.
|
|
72
|
+
*/
|
|
73
|
+
const displayedPaths = (
|
|
74
|
+
groups: readonly DiffGroup[],
|
|
75
|
+
displayOrder: DiffReadingOrderResponse | null,
|
|
76
|
+
): string[] => [
|
|
77
|
+
...new Set([
|
|
78
|
+
...(displayOrder?.groups ?? []).flatMap(({ files }) =>
|
|
79
|
+
files.flatMap(({ path, attached }) => [path, ...attached]),
|
|
80
|
+
),
|
|
81
|
+
...groups.flatMap(({ paths }) => paths),
|
|
82
|
+
]),
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
interface WithTestsInput {
|
|
86
|
+
paths: readonly string[];
|
|
87
|
+
code: readonly string[];
|
|
88
|
+
/** a test's subject as the reading order attached it */
|
|
89
|
+
readSubjects: ReadonlyMap<string, string>;
|
|
90
|
+
byPosition: (left: string, right: string) => number;
|
|
91
|
+
nameOf: (path: string, subject: string | null) => OverviewName;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Every file of a workspace, each followed by its tests, whatever the order
|
|
96
|
+
* of the diff: the reading order's subject, else one named like it
|
|
97
|
+
* (`subjectsOf`). A test covering no changed file comes last.
|
|
98
|
+
*/
|
|
99
|
+
const withTests = ({
|
|
100
|
+
paths,
|
|
101
|
+
code,
|
|
102
|
+
readSubjects,
|
|
103
|
+
byPosition,
|
|
104
|
+
nameOf,
|
|
105
|
+
}: WithTestsInput): OverviewName[] => {
|
|
106
|
+
const codeSet = new Set(code);
|
|
107
|
+
const testsOf = new Map<string, string[]>();
|
|
108
|
+
const uncovered: string[] = [];
|
|
109
|
+
for (const test of paths.filter(isAttachablePath)) {
|
|
110
|
+
const subject =
|
|
111
|
+
readSubjects.get(test) ??
|
|
112
|
+
subjectsOf(test).find((candidate) => codeSet.has(candidate));
|
|
113
|
+
if (subject === undefined || !codeSet.has(subject)) uncovered.push(test);
|
|
114
|
+
else testsOf.set(subject, [...(testsOf.get(subject) ?? []), test]);
|
|
115
|
+
}
|
|
116
|
+
return [
|
|
117
|
+
...code
|
|
118
|
+
.toSorted(byPosition)
|
|
119
|
+
.flatMap((path) => [
|
|
120
|
+
nameOf(path, null),
|
|
121
|
+
...(testsOf.get(path) ?? [])
|
|
122
|
+
.toSorted(byPosition)
|
|
123
|
+
.map((test) => nameOf(test, path)),
|
|
124
|
+
]),
|
|
125
|
+
...uncovered.toSorted(byPosition).map((test) => nameOf(test, null)),
|
|
126
|
+
];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const summarize = ({
|
|
130
|
+
groups,
|
|
131
|
+
omitted,
|
|
132
|
+
readingOrder,
|
|
133
|
+
displayOrder,
|
|
134
|
+
every,
|
|
135
|
+
}: SummarizeInput): OverviewWorkspace[] => {
|
|
136
|
+
const groupNames = [
|
|
137
|
+
...new Set([
|
|
138
|
+
...(displayOrder?.groups ?? []).map(({ name }) => name),
|
|
139
|
+
...groups.map(({ name }) => name),
|
|
140
|
+
]),
|
|
141
|
+
];
|
|
142
|
+
const pathsByWorkspace = new Map<string, string[]>(
|
|
143
|
+
groupNames.map((name) => [name, []]),
|
|
144
|
+
);
|
|
145
|
+
for (const { name, paths } of groups) {
|
|
146
|
+
pathsByWorkspace.get(name)!.push(...paths);
|
|
147
|
+
}
|
|
148
|
+
for (const { path } of omitted) {
|
|
149
|
+
const name = workspaceOf(path, groupNames);
|
|
150
|
+
pathsByWorkspace.set(name, [...(pathsByWorkspace.get(name) ?? []), path]);
|
|
151
|
+
}
|
|
152
|
+
const readingFiles = new Map(
|
|
153
|
+
(readingOrder?.groups ?? []).map(({ name, files }) => [name, files]),
|
|
154
|
+
);
|
|
155
|
+
const position = new Map(
|
|
156
|
+
displayedPaths(groups, displayOrder).map((path, index) => [path, index]),
|
|
157
|
+
);
|
|
158
|
+
const byPosition = (left: string, right: string): number =>
|
|
159
|
+
(position.get(left) ?? Infinity) - (position.get(right) ?? Infinity) || 0;
|
|
160
|
+
const entries = new Set(
|
|
161
|
+
(readingOrder?.groups ?? []).flatMap(({ files }) =>
|
|
162
|
+
files.filter(({ entry }) => entry).map(({ path }) => path),
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
const readSubjects = new Map(
|
|
166
|
+
(readingOrder?.groups ?? []).flatMap(({ files }) =>
|
|
167
|
+
files.flatMap(({ path, attached }) =>
|
|
168
|
+
attached.map((test) => [test, path] as const),
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
const nameOf = (path: string, subject: string | null): OverviewName => ({
|
|
173
|
+
path,
|
|
174
|
+
entry: entries.has(path),
|
|
175
|
+
shown: position.has(path),
|
|
176
|
+
subject,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return [...pathsByWorkspace]
|
|
180
|
+
.filter(([, paths]) => paths.length > 0)
|
|
181
|
+
.map(([name, paths]) => {
|
|
182
|
+
const code = paths.filter((path) => !isAttachablePath(path));
|
|
183
|
+
const roots = (readingFiles.get(name) ?? []).filter(({ root }) => root);
|
|
184
|
+
const names = ((): OverviewName[] => {
|
|
185
|
+
if (every) {
|
|
186
|
+
return withTests({ paths, code, readSubjects, byPosition, nameOf });
|
|
187
|
+
}
|
|
188
|
+
const listed = roots.length > 0 ? roots.map(({ path }) => path) : code;
|
|
189
|
+
return listed.toSorted(byPosition).map((path) => nameOf(path, null));
|
|
190
|
+
})();
|
|
191
|
+
return {
|
|
192
|
+
name,
|
|
193
|
+
files: code.length,
|
|
194
|
+
tests: paths.length - code.length,
|
|
195
|
+
names,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const relativeTo = (workspace: string, path: string): string =>
|
|
201
|
+
workspace !== "" && path.startsWith(`${workspace}/`)
|
|
202
|
+
? path.slice(workspace.length + 1)
|
|
203
|
+
: path;
|
|
204
|
+
|
|
205
|
+
/** A test listed under its subject is named from the subject's directory. */
|
|
206
|
+
const labelOf = (
|
|
207
|
+
workspace: string,
|
|
208
|
+
path: string,
|
|
209
|
+
subject: string | null,
|
|
210
|
+
): string => {
|
|
211
|
+
if (subject === null) return relativeTo(workspace, path);
|
|
212
|
+
const directory = subject.slice(0, subject.lastIndexOf("/"));
|
|
213
|
+
return relativeTo(directory, path);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const typescriptFile = /\.[cm]?tsx?$/;
|
|
217
|
+
|
|
218
|
+
function FileIcon({ path, test }: { path: string; test: boolean }): ReactNode {
|
|
219
|
+
if (test) {
|
|
220
|
+
return (
|
|
221
|
+
<Icon icon={<TestTubeRegularIcon />} size={16} className="text-muted" />
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (typescriptFile.test(path)) {
|
|
225
|
+
return (
|
|
226
|
+
<Icon icon={<FileTsRegularIcon />} size={16} className="text-muted" />
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return <Icon icon={<FileRegularIcon />} size={16} className="text-muted" />;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
interface OverviewFileProps {
|
|
233
|
+
path: string;
|
|
234
|
+
/** what is shown of the path: from its workspace, or its subject's directory */
|
|
235
|
+
label: string;
|
|
236
|
+
entry: boolean;
|
|
237
|
+
test: boolean;
|
|
238
|
+
indented: boolean;
|
|
239
|
+
/** null: not in the diff below (an omitted file), nothing to go to */
|
|
240
|
+
onPress: (() => void) | null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The file's name first, its directory under it, so the names line up. */
|
|
244
|
+
function OverviewFile({
|
|
245
|
+
path,
|
|
246
|
+
label,
|
|
247
|
+
entry,
|
|
248
|
+
test,
|
|
249
|
+
indented,
|
|
250
|
+
onPress,
|
|
251
|
+
}: OverviewFileProps): ReactNode {
|
|
252
|
+
const slash = label.lastIndexOf("/");
|
|
253
|
+
const directory = label.slice(0, Math.max(slash, 0));
|
|
254
|
+
const content = (
|
|
255
|
+
<View className="flex-row items-start gap-xs">
|
|
256
|
+
<View className="pt-[2px]">
|
|
257
|
+
<FileIcon path={path} test={test} />
|
|
258
|
+
</View>
|
|
259
|
+
<View className="min-w-0 shrink gap-[1px]">
|
|
260
|
+
{/* one Text, so "entry" sits on the name's baseline */}
|
|
261
|
+
<Text
|
|
262
|
+
className={
|
|
263
|
+
test ? "text-sm text-muted" : "font-body-bold text-sm text-sharp"
|
|
264
|
+
}
|
|
265
|
+
>
|
|
266
|
+
{label.slice(slash + 1)}
|
|
267
|
+
{entry ? (
|
|
268
|
+
<Text className="font-body text-xs text-muted"> · entry</Text>
|
|
269
|
+
) : null}
|
|
270
|
+
</Text>
|
|
271
|
+
{directory === "" ? null : (
|
|
272
|
+
<Text className="text-xs text-muted">{directory}</Text>
|
|
273
|
+
)}
|
|
274
|
+
</View>
|
|
275
|
+
</View>
|
|
276
|
+
);
|
|
277
|
+
const frame = indented
|
|
278
|
+
? "ml-l rounded-sm px-xs py-xxs"
|
|
279
|
+
: "rounded-sm px-xs py-xxs";
|
|
280
|
+
if (onPress === null) return <View className={frame}>{content}</View>;
|
|
281
|
+
return (
|
|
282
|
+
<PressableBox
|
|
283
|
+
variant="soft"
|
|
284
|
+
accent="neutral"
|
|
285
|
+
aria-label={`Go to ${path}`}
|
|
286
|
+
className={frame}
|
|
287
|
+
onPress={onPress}
|
|
288
|
+
>
|
|
289
|
+
{content}
|
|
290
|
+
</PressableBox>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const statusNotes: Record<OverviewReadingOrderStatus, string | null> = {
|
|
295
|
+
ready: null,
|
|
296
|
+
none: null,
|
|
297
|
+
reading: "Reading the entry points: files are listed meanwhile.",
|
|
298
|
+
failed: "The entry points could not be read: files are listed instead.",
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export interface PullRequestDiffOverviewProps {
|
|
302
|
+
/** `PullRequestDiffResponse.groups` of the range shown */
|
|
303
|
+
groups: DiffGroup[];
|
|
304
|
+
/** `PullRequestDiffResponse.omitted`: counted in their workspace */
|
|
305
|
+
omitted: DiffOmittedFile[];
|
|
306
|
+
/** where the roots and entry points come from; null until it is read */
|
|
307
|
+
readingOrder: DiffReadingOrderResponse | null;
|
|
308
|
+
readingOrderStatus: OverviewReadingOrderStatus;
|
|
309
|
+
/** what the touched exports are used by outside the diff; null until it is read */
|
|
310
|
+
impacts?: DiffImpactsResponse | null;
|
|
311
|
+
impactsStatus?: ImpactsStatus;
|
|
312
|
+
/** the reading order when the viewer orders the files by it, null in package order */
|
|
313
|
+
displayOrder: DiffReadingOrderResponse | null;
|
|
314
|
+
/** brings the file into view in the diff below */
|
|
315
|
+
onFilePress: (path: string) => void;
|
|
316
|
+
/**
|
|
317
|
+
* In a column beside the diff, which scrolls on its own: every file, one a
|
|
318
|
+
* line, its tests indented below it, with no cap on their number.
|
|
319
|
+
*/
|
|
320
|
+
aside: boolean;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* What the range touches, by workspace: how many files (tests apart), then
|
|
325
|
+
* the names to start reading from, its roots, else its files (every file
|
|
326
|
+
* aside the diff), in the order the diff below shows them, each a link to its
|
|
327
|
+
* file. Static content on the screen ground, with no surface of its own.
|
|
328
|
+
*/
|
|
329
|
+
export function PullRequestDiffOverview({
|
|
330
|
+
groups,
|
|
331
|
+
omitted,
|
|
332
|
+
readingOrder,
|
|
333
|
+
readingOrderStatus,
|
|
334
|
+
impacts = null,
|
|
335
|
+
impactsStatus = "none",
|
|
336
|
+
displayOrder,
|
|
337
|
+
onFilePress,
|
|
338
|
+
aside,
|
|
339
|
+
}: PullRequestDiffOverviewProps): ReactNode {
|
|
340
|
+
const workspaces = summarize({
|
|
341
|
+
groups,
|
|
342
|
+
omitted,
|
|
343
|
+
readingOrder,
|
|
344
|
+
displayOrder,
|
|
345
|
+
every: aside,
|
|
346
|
+
});
|
|
347
|
+
if (workspaces.length === 0) return null;
|
|
348
|
+
const totalNames = workspaces.reduce(
|
|
349
|
+
(total, { names }) => total + names.length,
|
|
350
|
+
0,
|
|
351
|
+
);
|
|
352
|
+
const showNames = aside || totalNames <= maxOverviewNames;
|
|
353
|
+
const note = statusNotes[readingOrderStatus];
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<View className="gap-l py-xs" role="region" aria-label="Changes overview">
|
|
357
|
+
<View className="flex-row items-baseline gap-xs">
|
|
358
|
+
<Text className="font-heading-bold text-lg text-sharp">Changes</Text>
|
|
359
|
+
<Text className="text-sm text-muted">
|
|
360
|
+
{`in ${pluralOf(workspaces.length, "workspace")}`}
|
|
361
|
+
</Text>
|
|
362
|
+
</View>
|
|
363
|
+
{workspaces.map(({ name, files, tests, names }) => (
|
|
364
|
+
<View key={name} className="gap-sm">
|
|
365
|
+
<View className="gap-xxs">
|
|
366
|
+
<View className="flex-row items-center gap-xs">
|
|
367
|
+
<Icon
|
|
368
|
+
icon={<PackageDuotoneIcon />}
|
|
369
|
+
size={20}
|
|
370
|
+
className="text-accent"
|
|
371
|
+
/>
|
|
372
|
+
<Text className="shrink font-heading-bold text-base text-sharp">
|
|
373
|
+
{name === "" ? "Root" : name}
|
|
374
|
+
</Text>
|
|
375
|
+
</View>
|
|
376
|
+
<Text className="pl-[28px] text-xs text-muted">
|
|
377
|
+
{tests === 0
|
|
378
|
+
? pluralOf(files, "file")
|
|
379
|
+
: `${pluralOf(files, "file")} · ${pluralOf(tests, "test")}`}
|
|
380
|
+
</Text>
|
|
381
|
+
</View>
|
|
382
|
+
{showNames && names.length > 0 ? (
|
|
383
|
+
<View className={aside ? "gap-xxs" : "flex-row flex-wrap gap-xs"}>
|
|
384
|
+
{names.map(({ path, entry, shown, subject }) => (
|
|
385
|
+
<OverviewFile
|
|
386
|
+
key={path}
|
|
387
|
+
path={path}
|
|
388
|
+
label={labelOf(name, path, subject)}
|
|
389
|
+
entry={entry}
|
|
390
|
+
test={isAttachablePath(path)}
|
|
391
|
+
indented={subject !== null}
|
|
392
|
+
onPress={
|
|
393
|
+
shown
|
|
394
|
+
? () => {
|
|
395
|
+
onFilePress(path);
|
|
396
|
+
}
|
|
397
|
+
: null
|
|
398
|
+
}
|
|
399
|
+
/>
|
|
400
|
+
))}
|
|
401
|
+
</View>
|
|
402
|
+
) : null}
|
|
403
|
+
</View>
|
|
404
|
+
))}
|
|
405
|
+
{showNames ? null : (
|
|
406
|
+
<Text className="text-xs text-muted">
|
|
407
|
+
{`Names are left out past ${String(maxOverviewNames)}.`}
|
|
408
|
+
</Text>
|
|
409
|
+
)}
|
|
410
|
+
{note === null ? null : (
|
|
411
|
+
<Text className="text-xs text-muted">{note}</Text>
|
|
412
|
+
)}
|
|
413
|
+
<PullRequestDiffImpactsSummary impacts={impacts} status={impactsStatus} />
|
|
414
|
+
</View>
|
|
415
|
+
);
|
|
416
|
+
}
|