@reviewflow/ui 6.23.0 → 6.25.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/package.json +21 -6
- package/src/components/branch-diff/diff-font.ts +26 -0
- package/src/components/branch-diff/diff-themes.ts +53 -1
- package/src/components/branch-diff/reviewflow-diff-selection.ts +11 -0
- package/src/components/branch-diff/reviewflow-diff-themes.ts +2218 -0
- package/src/components/branch-diff/use-diff-settings.tsx +15 -3
- package/src/components/branch-diff/whitespace-button.tsx +2 -2
- package/src/components/diff-settings-section.tsx +189 -0
- package/src/components/diff-theme-preview.tsx +63 -0
- package/src/components/file-kind.ts +91 -0
- package/src/components/file-type-icon.tsx +70 -0
- package/src/components/pull-request-actions-review.tsx +2 -2
- package/src/components/pull-request-commits.tsx +2 -1
- package/src/components/pull-request-diff-annotations.tsx +8 -5
- package/src/components/pull-request-diff-impacts-summary.tsx +2 -1
- package/src/components/pull-request-diff-impacts.ts +1 -3
- package/src/components/pull-request-diff-jump.ts +59 -1
- package/src/components/pull-request-diff-overview-tree.ts +57 -0
- package/src/components/pull-request-diff-overview.tsx +489 -90
- package/src/components/pull-request-diff-tree-files.ts +64 -0
- package/src/components/pull-request-diff-tree.tsx +232 -0
- package/src/components/pull-request-diff-view.tsx +112 -28
- package/src/components/pull-request-diff-viewer.tsx +26 -6
- package/src/components/pull-request-files.tsx +6 -6
- package/src/components/pull-request-panel.tsx +23 -21
- package/src/components/pull-request-reactions.tsx +2 -2
- package/src/components/pull-request-revisions.tsx +2 -1
- package/src/components/pull-request-screen.tsx +37 -17
- package/src/components/pull-request-suggestion-actions.tsx +7 -6
- package/src/components/repository-attachment-banner.tsx +1 -1
- package/src/components/scroll-parent.ts +20 -0
- package/src/components/tooltip-icon-button.tsx +27 -0
- package/src/editor/code-editor.tsx +1 -0
- package/src/editor/code-token-colors.css +39 -0
- package/src/editor/codeHighlight.ts +2 -1
- package/src/editor/markdown-editor.css +1 -34
- package/src/editor/markdown-editor.tsx +1 -0
- package/src/index.ts +6 -6
- package/src/palette.ts +22 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PullRequestDiffResponse } from "@reviewflow/sdk";
|
|
2
|
+
|
|
3
|
+
export type DiffTreeFileStatus = "added" | "deleted" | "modified" | "renamed";
|
|
4
|
+
|
|
5
|
+
export interface DiffTreeFile {
|
|
6
|
+
path: string;
|
|
7
|
+
/** null for an omitted file, whose header the patch does not carry */
|
|
8
|
+
status: DiffTreeFileStatus | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const fileHeader = /^diff --git /mu;
|
|
12
|
+
const samePathHeader = /^a\/(.+) b\/\1$/u;
|
|
13
|
+
const renameTo = /^rename to (.+)$/mu;
|
|
14
|
+
const newFile = /^new file mode /mu;
|
|
15
|
+
const deletedFile = /^deleted file mode /mu;
|
|
16
|
+
|
|
17
|
+
const statusOfBlock = (block: string): DiffTreeFileStatus => {
|
|
18
|
+
if (newFile.test(block)) return "added";
|
|
19
|
+
if (deletedFile.test(block)) return "deleted";
|
|
20
|
+
if (renameTo.test(block)) return "renamed";
|
|
21
|
+
return "modified";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const pathOfBlock = (block: string): string | undefined => {
|
|
25
|
+
const renamed = renameTo.exec(block)?.[1];
|
|
26
|
+
if (renamed !== undefined) return renamed;
|
|
27
|
+
const firstLine = block.slice(0, block.indexOf("\n"));
|
|
28
|
+
return samePathHeader.exec(firstLine)?.[1];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Read from the patch's git headers, keyed by the path after any rename. */
|
|
32
|
+
export const fileStatusesOf = (
|
|
33
|
+
patch: string,
|
|
34
|
+
): ReadonlyMap<string, DiffTreeFileStatus> =>
|
|
35
|
+
new Map(
|
|
36
|
+
patch
|
|
37
|
+
.split(fileHeader)
|
|
38
|
+
.slice(1)
|
|
39
|
+
.flatMap((block) => {
|
|
40
|
+
const path = pathOfBlock(block);
|
|
41
|
+
return path === undefined
|
|
42
|
+
? []
|
|
43
|
+
: [[path, statusOfBlock(block)] as const];
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every file of the range: the patch's, with their status, then the omitted
|
|
49
|
+
* ones. A patch file whose header cannot be read counts as modified.
|
|
50
|
+
*/
|
|
51
|
+
export const diffTreeFilesOf = (
|
|
52
|
+
diff: Pick<PullRequestDiffResponse, "groups" | "omitted" | "patch">,
|
|
53
|
+
): DiffTreeFile[] => {
|
|
54
|
+
const statuses = fileStatusesOf(diff.patch);
|
|
55
|
+
return [
|
|
56
|
+
...diff.groups.flatMap(({ paths }) =>
|
|
57
|
+
paths.map((path) => ({
|
|
58
|
+
path,
|
|
59
|
+
status: statuses.get(path) ?? "modified",
|
|
60
|
+
})),
|
|
61
|
+
),
|
|
62
|
+
...diff.omitted.map(({ path }) => ({ path, status: null })),
|
|
63
|
+
];
|
|
64
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { getResolvedOrResolveTheme } from "@pierre/diffs";
|
|
2
|
+
import type {
|
|
3
|
+
FileTree as FileTreeModel,
|
|
4
|
+
FileTreeRowDecoration,
|
|
5
|
+
GitStatusEntry,
|
|
6
|
+
} from "@pierre/trees";
|
|
7
|
+
import { themeToTreeStyles } from "@pierre/trees";
|
|
8
|
+
import { FileTree, useFileTree } from "@pierre/trees/react";
|
|
9
|
+
import type { AccentOrNeutral } from "alouette";
|
|
10
|
+
import { AccentScope, useCurrentMode } from "alouette";
|
|
11
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
12
|
+
import { useEffect, useRef, useState } from "react";
|
|
13
|
+
import type { DiffTheme } from "./branch-diff/diff-themes.ts";
|
|
14
|
+
import { diffThemeNames } from "./branch-diff/diff-themes.ts";
|
|
15
|
+
import type { DiffTreeFile } from "./pull-request-diff-tree-files.ts";
|
|
16
|
+
|
|
17
|
+
export interface PullRequestDiffTreeProps {
|
|
18
|
+
files: DiffTreeFile[];
|
|
19
|
+
viewedPaths: readonly string[];
|
|
20
|
+
/** the diff's highlighting theme, whose git colors the statuses take */
|
|
21
|
+
theme: DiffTheme;
|
|
22
|
+
/** brings the file into view in the diff */
|
|
23
|
+
onFilePress: (path: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DecorationData {
|
|
27
|
+
viewedPaths: ReadonlySet<string>;
|
|
28
|
+
omittedPaths: ReadonlySet<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type CustomProperties = CSSProperties & Record<`--${string}`, string>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Custom properties cross the shadow root, and a `var()` in one resolves where
|
|
35
|
+
* it is declared, inside alouette's theme: the tree takes the screen's tokens.
|
|
36
|
+
* A status takes the diff theme's git color, as the diff's file headers do, and
|
|
37
|
+
* the color of its accent when the theme has none (`CapturedAccent`).
|
|
38
|
+
*/
|
|
39
|
+
const hostStyle = (
|
|
40
|
+
mode: "dark" | "light",
|
|
41
|
+
gitColors: CustomProperties,
|
|
42
|
+
): CustomProperties => ({
|
|
43
|
+
...gitColors,
|
|
44
|
+
height: "100%",
|
|
45
|
+
colorScheme: mode,
|
|
46
|
+
"--trees-bg-override": "transparent",
|
|
47
|
+
"--trees-fg-override": "var(--color-sharp)",
|
|
48
|
+
"--trees-fg-muted-override": "var(--color-muted)",
|
|
49
|
+
"--trees-bg-muted-override": "var(--color-interactive-soft-hover)",
|
|
50
|
+
"--trees-selected-bg-override": "var(--color-interactive-soft-active)",
|
|
51
|
+
"--trees-selected-fg-override": "var(--color-sharp)",
|
|
52
|
+
"--trees-border-color-override": "var(--color-border-muted)",
|
|
53
|
+
"--trees-indent-guide-bg-override": "var(--color-border-muted)",
|
|
54
|
+
"--trees-input-bg-override": "var(--color-lowered)",
|
|
55
|
+
"--trees-search-bg-override": "var(--color-lowered)",
|
|
56
|
+
"--trees-search-fg-override": "var(--color-sharp)",
|
|
57
|
+
"--trees-accent-override": "var(--tree-brand)",
|
|
58
|
+
"--trees-focus-ring-color-override": "var(--tree-brand)",
|
|
59
|
+
"--trees-status-added-override":
|
|
60
|
+
"var(--trees-theme-git-added-fg, var(--tree-success))",
|
|
61
|
+
"--trees-status-untracked-override":
|
|
62
|
+
"var(--trees-theme-git-untracked-fg, var(--tree-success))",
|
|
63
|
+
"--trees-status-modified-override":
|
|
64
|
+
"var(--trees-theme-git-modified-fg, var(--tree-info))",
|
|
65
|
+
"--trees-status-renamed-override":
|
|
66
|
+
"var(--trees-theme-git-renamed-fg, var(--tree-warning))",
|
|
67
|
+
"--trees-status-deleted-override":
|
|
68
|
+
"var(--trees-theme-git-deleted-fg, var(--tree-danger))",
|
|
69
|
+
"--trees-font-family-override": "var(--font-body)",
|
|
70
|
+
"--trees-font-size-override": "var(--text-sm, 13px)",
|
|
71
|
+
"--trees-padding-inline-override": "0px",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Holds `accent`'s color as `--tree-<accent>` for the subtree, whatever accent it is under. */
|
|
75
|
+
function CapturedAccent({
|
|
76
|
+
accent,
|
|
77
|
+
children,
|
|
78
|
+
}: {
|
|
79
|
+
accent: Exclude<AccentOrNeutral, "neutral">;
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
}): ReactNode {
|
|
82
|
+
const style: CustomProperties = {
|
|
83
|
+
display: "contents",
|
|
84
|
+
[`--tree-${accent}`]: "var(--color-accent)",
|
|
85
|
+
};
|
|
86
|
+
return (
|
|
87
|
+
<AccentScope accent={accent}>
|
|
88
|
+
<div style={style}>{children}</div>
|
|
89
|
+
</AccentScope>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const gitColorsOf = (styles: Record<string, string>): CustomProperties =>
|
|
94
|
+
Object.fromEntries(
|
|
95
|
+
Object.entries(styles).filter(([key]) =>
|
|
96
|
+
key.startsWith("--trees-theme-git-"),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
interface ThemeGitColors {
|
|
101
|
+
themeName: string;
|
|
102
|
+
colors: CustomProperties;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Empty until the theme resolves, or when it cannot: the accents stand in. */
|
|
106
|
+
const useThemeGitColors = (themeName: string): CustomProperties => {
|
|
107
|
+
const [resolved, setResolved] = useState<ThemeGitColors | null>(null);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
let current = true;
|
|
110
|
+
Promise.resolve(getResolvedOrResolveTheme(themeName)).then(
|
|
111
|
+
(theme) => {
|
|
112
|
+
if (current) {
|
|
113
|
+
setResolved({
|
|
114
|
+
themeName,
|
|
115
|
+
colors: gitColorsOf(themeToTreeStyles(theme)),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
() => {
|
|
120
|
+
if (current) setResolved({ themeName, colors: {} });
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
return () => {
|
|
124
|
+
current = false;
|
|
125
|
+
};
|
|
126
|
+
}, [themeName]);
|
|
127
|
+
return resolved?.themeName === themeName ? resolved.colors : {};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const gitStatusOf = (files: readonly DiffTreeFile[]): GitStatusEntry[] =>
|
|
131
|
+
files.flatMap(({ path, status }) =>
|
|
132
|
+
status === null ? [] : [{ path, status }],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const decorationOf = (
|
|
136
|
+
path: string,
|
|
137
|
+
{ viewedPaths, omittedPaths }: DecorationData,
|
|
138
|
+
): FileTreeRowDecoration | null => {
|
|
139
|
+
if (omittedPaths.has(path)) {
|
|
140
|
+
return { text: "left out", title: "Not in the diff: listed above it" };
|
|
141
|
+
}
|
|
142
|
+
if (viewedPaths.has(path)) return { text: "viewed", title: "Marked viewed" };
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The range's files by folder, in `@pierre/trees`: searchable, virtualized,
|
|
148
|
+
* each file's git status and viewed mark on its row. Web only (DOM in a shadow
|
|
149
|
+
* root), loaded lazily as the diff viewer is.
|
|
150
|
+
*
|
|
151
|
+
* The model is created once and fed through its methods. Selecting a file
|
|
152
|
+
* scrolls the diff to it and is undone at once, so pressing the same file
|
|
153
|
+
* again scrolls again. The row decorations have no setter: they read the
|
|
154
|
+
* latest data from a ref, and resetting the git status repaints them.
|
|
155
|
+
*/
|
|
156
|
+
function PullRequestDiffTree({
|
|
157
|
+
files,
|
|
158
|
+
viewedPaths: viewedList,
|
|
159
|
+
theme,
|
|
160
|
+
onFilePress,
|
|
161
|
+
}: PullRequestDiffTreeProps): ReactNode {
|
|
162
|
+
const mode = useCurrentMode();
|
|
163
|
+
const gitColors = useThemeGitColors(diffThemeNames(theme)[mode]);
|
|
164
|
+
const paths = files.map(({ path }) => path);
|
|
165
|
+
const pathsKey = paths.join("\n");
|
|
166
|
+
const repaintKey = [
|
|
167
|
+
...files.map(({ path, status }) => `${status ?? "omitted"} ${path}`),
|
|
168
|
+
...viewedList,
|
|
169
|
+
].join("\n");
|
|
170
|
+
const viewedPaths = new Set(viewedList);
|
|
171
|
+
const omittedPaths = new Set(
|
|
172
|
+
files.filter(({ status }) => status === null).map(({ path }) => path),
|
|
173
|
+
);
|
|
174
|
+
const latest = useRef({ files, onFilePress, viewedPaths, omittedPaths });
|
|
175
|
+
const filePaths = useRef(new Set(paths));
|
|
176
|
+
const modelRef = useRef<FileTreeModel | null>(null);
|
|
177
|
+
const appliedPathsKey = useRef(pathsKey);
|
|
178
|
+
|
|
179
|
+
const { model } = useFileTree({
|
|
180
|
+
paths,
|
|
181
|
+
flattenEmptyDirectories: true,
|
|
182
|
+
initialExpansion: "open",
|
|
183
|
+
density: "compact",
|
|
184
|
+
icons: { set: "standard", colored: false },
|
|
185
|
+
search: true,
|
|
186
|
+
fileTreeSearchMode: "hide-non-matches",
|
|
187
|
+
gitStatus: gitStatusOf(files),
|
|
188
|
+
renderRowDecoration: ({ item }) =>
|
|
189
|
+
item.kind === "file" ? decorationOf(item.path, latest.current) : null,
|
|
190
|
+
onSelectionChange: (selected) => {
|
|
191
|
+
const path = selected.at(-1);
|
|
192
|
+
if (path === undefined || !filePaths.current.has(path)) return;
|
|
193
|
+
latest.current.onFilePress(path);
|
|
194
|
+
queueMicrotask(() => {
|
|
195
|
+
modelRef.current?.getItem(path)?.deselect();
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
modelRef.current = model;
|
|
202
|
+
latest.current = { files, onFilePress, viewedPaths, omittedPaths };
|
|
203
|
+
filePaths.current = new Set(paths);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const current = latest.current.files;
|
|
208
|
+
if (appliedPathsKey.current !== pathsKey) {
|
|
209
|
+
appliedPathsKey.current = pathsKey;
|
|
210
|
+
model.resetPaths(current.map(({ path }) => path));
|
|
211
|
+
}
|
|
212
|
+
model.setGitStatus(gitStatusOf(current));
|
|
213
|
+
}, [model, pathsKey, repaintKey]);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<CapturedAccent accent="brand">
|
|
217
|
+
<CapturedAccent accent="success">
|
|
218
|
+
<CapturedAccent accent="info">
|
|
219
|
+
<CapturedAccent accent="warning">
|
|
220
|
+
<CapturedAccent accent="danger">
|
|
221
|
+
<AccentScope accent="neutral">
|
|
222
|
+
<FileTree model={model} style={hostStyle(mode, gitColors)} />
|
|
223
|
+
</AccentScope>
|
|
224
|
+
</CapturedAccent>
|
|
225
|
+
</CapturedAccent>
|
|
226
|
+
</CapturedAccent>
|
|
227
|
+
</CapturedAccent>
|
|
228
|
+
</CapturedAccent>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default PullRequestDiffTree;
|
|
@@ -7,10 +7,11 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
AccentScope,
|
|
9
9
|
Code,
|
|
10
|
-
IconButton,
|
|
11
10
|
IndeterminateCircularProgress,
|
|
12
11
|
IndeterminateLinearProgress,
|
|
13
12
|
InfoMessage,
|
|
13
|
+
Tab,
|
|
14
|
+
Tabs,
|
|
14
15
|
Text,
|
|
15
16
|
View,
|
|
16
17
|
WarningMessage,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
useRef,
|
|
32
33
|
useState,
|
|
33
34
|
} from "react";
|
|
35
|
+
import { Platform } from "react-native";
|
|
34
36
|
import type { PullRequestDiffRefusal } from "../dataSource.ts";
|
|
35
37
|
import { usePullRequestReviewDraft } from "../services/PullRequestReviewDraftProvider.tsx";
|
|
36
38
|
import { usePullRequestSlots } from "../slots.tsx";
|
|
@@ -51,20 +53,30 @@ import type { ImpactsStatus } from "./pull-request-diff-impacts.ts";
|
|
|
51
53
|
import {
|
|
52
54
|
DiffFileElementsContext,
|
|
53
55
|
createDiffFileElements,
|
|
56
|
+
useFileInView,
|
|
54
57
|
} from "./pull-request-diff-jump.ts";
|
|
55
58
|
import type { Composer } from "./pull-request-diff-lines.ts";
|
|
56
59
|
import type { OverviewReadingOrderStatus } from "./pull-request-diff-overview.tsx";
|
|
57
60
|
import { PullRequestDiffOverview } from "./pull-request-diff-overview.tsx";
|
|
61
|
+
import { diffTreeFilesOf } from "./pull-request-diff-tree-files.ts";
|
|
58
62
|
import type {
|
|
59
63
|
DiffStyle,
|
|
60
64
|
PullRequestDiffReview,
|
|
61
65
|
} from "./pull-request-diff-viewer.tsx";
|
|
62
66
|
import { cssVariableOf, scrollToTopOf } from "./scroll-parent.ts";
|
|
67
|
+
import { TooltipIconButton } from "./tooltip-icon-button.tsx";
|
|
63
68
|
|
|
64
69
|
const PullRequestDiffViewer = lazy(
|
|
65
70
|
() => import("./pull-request-diff-viewer.tsx"),
|
|
66
71
|
);
|
|
67
72
|
|
|
73
|
+
const PullRequestDiffTree = lazy(() => import("./pull-request-diff-tree.tsx"));
|
|
74
|
+
|
|
75
|
+
/** the tree renders DOM in a shadow root */
|
|
76
|
+
const diffTreeAvailable = Platform.OS === "web";
|
|
77
|
+
|
|
78
|
+
type AsideView = "list" | "tree";
|
|
79
|
+
|
|
68
80
|
const omissionLabel = (reason: DiffOmittedFile["reason"]): string => {
|
|
69
81
|
if (reason === "binary") return "binary";
|
|
70
82
|
if (reason === "tooLarge") return "too large";
|
|
@@ -209,6 +221,7 @@ export function PullRequestDiffView({
|
|
|
209
221
|
const [toolbarHeight, setToolbarHeight] = useState(0);
|
|
210
222
|
const fileElements = useMemo(createDiffFileElements, []);
|
|
211
223
|
const highlightProbe = useRef<ComponentRef<typeof View>>(null);
|
|
224
|
+
const toolbar = useRef<ComponentRef<typeof View>>(null);
|
|
212
225
|
// a JS breakpoint rather than `xl:` classes: the sticky offset is the measured toolbar's
|
|
213
226
|
const aside = useCurrentBreakpointNameFiltered(["base", "wide"]) === "wide";
|
|
214
227
|
const whitespace = useWhitespaceToggle();
|
|
@@ -244,7 +257,8 @@ export function PullRequestDiffView({
|
|
|
244
257
|
(shownCommitComposer !== null ||
|
|
245
258
|
(review.commitOid === undefined && draft.composer !== null));
|
|
246
259
|
const mode = useCurrentMode();
|
|
247
|
-
const { wordWrap, theme, fileOrder, setFileOrder } =
|
|
260
|
+
const { wordWrap, theme, fontFamily, fileOrder, setFileOrder } =
|
|
261
|
+
useDiffSettings();
|
|
248
262
|
const missingCheckout = useMissingCheckoutReason(range);
|
|
249
263
|
const readingOrder = usePullRequestReadingOrder(
|
|
250
264
|
range,
|
|
@@ -267,16 +281,59 @@ export function PullRequestDiffView({
|
|
|
267
281
|
if (readingOrder.error !== null) return "failed";
|
|
268
282
|
return "none";
|
|
269
283
|
})();
|
|
284
|
+
const [requestedAsideView, setRequestedAsideView] =
|
|
285
|
+
useState<AsideView | null>(null);
|
|
286
|
+
const asideView = ((): AsideView => {
|
|
287
|
+
if (!diffTreeAvailable) return "list";
|
|
288
|
+
if (requestedAsideView !== null) return requestedAsideView;
|
|
289
|
+
return ordered ? "list" : "tree";
|
|
290
|
+
})();
|
|
291
|
+
const listTab = (
|
|
292
|
+
<Tab
|
|
293
|
+
value="list"
|
|
294
|
+
label={readingOrder.available ? "Reading order" : "Workspaces"}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
const treeFiles = useMemo(
|
|
298
|
+
() => (shownDiff === null ? [] : diffTreeFilesOf(shownDiff)),
|
|
299
|
+
[shownDiff],
|
|
300
|
+
);
|
|
301
|
+
const statuses = useMemo(
|
|
302
|
+
() =>
|
|
303
|
+
new Map(
|
|
304
|
+
treeFiles.flatMap(({ path, status }) =>
|
|
305
|
+
status === null ? [] : [[path, status] as const],
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
[treeFiles],
|
|
309
|
+
);
|
|
310
|
+
const viewedPaths = (review?.files ?? [])
|
|
311
|
+
.filter(({ viewed }) => viewed === "viewed")
|
|
312
|
+
.map(({ path }) => path);
|
|
313
|
+
const fileInView = useFileInView(
|
|
314
|
+
fileElements,
|
|
315
|
+
toolbar,
|
|
316
|
+
aside && asideView === "list",
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const goToFile = (path: string): void => {
|
|
320
|
+
const element = fileElements.elementOf(path);
|
|
321
|
+
if (element === undefined) return;
|
|
322
|
+
scrollToTopOf(element, toolbarHeight);
|
|
323
|
+
const color = cssVariableOf(highlightProbe.current, "--color-enabled");
|
|
324
|
+
if (color !== "") fileElements.highlight(path, color);
|
|
325
|
+
};
|
|
270
326
|
|
|
271
327
|
return (
|
|
272
328
|
<View
|
|
273
329
|
className={
|
|
274
330
|
aside && diffStyle === "split"
|
|
275
|
-
? "gap-m web:relative web:left-1/2 web:w-[
|
|
331
|
+
? "gap-m web:relative web:left-1/2 web:w-[100cqw] web:-translate-x-1/2"
|
|
276
332
|
: "gap-m"
|
|
277
333
|
}
|
|
278
334
|
>
|
|
279
335
|
<View
|
|
336
|
+
ref={toolbar}
|
|
280
337
|
className="-mx-l flex-row flex-wrap items-center justify-between gap-xs bg-screen px-l py-sm web:sticky web:top-0 web:z-10"
|
|
281
338
|
onLayout={(event) => {
|
|
282
339
|
setToolbarHeight(event.nativeEvent.layout.height);
|
|
@@ -296,7 +353,7 @@ export function PullRequestDiffView({
|
|
|
296
353
|
{toolbarStart}
|
|
297
354
|
</View>
|
|
298
355
|
<View className="flex-row items-center gap-xs">
|
|
299
|
-
<
|
|
356
|
+
<TooltipIconButton
|
|
300
357
|
icon={
|
|
301
358
|
diffStyle === "split" ? (
|
|
302
359
|
<RowsRegularIcon />
|
|
@@ -313,7 +370,7 @@ export function PullRequestDiffView({
|
|
|
313
370
|
}}
|
|
314
371
|
/>
|
|
315
372
|
{readingOrder.available ? (
|
|
316
|
-
<
|
|
373
|
+
<TooltipIconButton
|
|
317
374
|
icon={<TreeStructureRegularIcon />}
|
|
318
375
|
size="sm"
|
|
319
376
|
accent={ordered ? "brand" : "neutral"}
|
|
@@ -325,6 +382,7 @@ export function PullRequestDiffView({
|
|
|
325
382
|
}
|
|
326
383
|
onPress={() => {
|
|
327
384
|
setFileOrder(ordered ? "packages" : "reading");
|
|
385
|
+
setRequestedAsideView(null);
|
|
328
386
|
}}
|
|
329
387
|
/>
|
|
330
388
|
) : null}
|
|
@@ -335,13 +393,13 @@ export function PullRequestDiffView({
|
|
|
335
393
|
}
|
|
336
394
|
onToggle={whitespace.toggle}
|
|
337
395
|
/>
|
|
338
|
-
<
|
|
396
|
+
<TooltipIconButton
|
|
339
397
|
icon={<ArrowsOutLineVerticalRegularIcon />}
|
|
340
398
|
size="sm"
|
|
341
399
|
aria-label="Open every file"
|
|
342
400
|
onPress={collapse.expandAll}
|
|
343
401
|
/>
|
|
344
|
-
<
|
|
402
|
+
<TooltipIconButton
|
|
345
403
|
icon={<ArrowsInLineVerticalRegularIcon />}
|
|
346
404
|
size="sm"
|
|
347
405
|
aria-label="Close every file"
|
|
@@ -369,31 +427,56 @@ export function PullRequestDiffView({
|
|
|
369
427
|
<View
|
|
370
428
|
className={
|
|
371
429
|
aside
|
|
372
|
-
? "w-[280px] shrink-0 web:sticky web:max-h-[calc(100vh-8rem)] web:overflow-y-auto"
|
|
430
|
+
? "w-[280px] shrink-0 gap-sm web:sticky web:max-h-[calc(100vh-8rem)] web:overflow-y-auto"
|
|
373
431
|
: ""
|
|
374
432
|
}
|
|
375
433
|
style={aside ? { top: toolbarHeight } : undefined}
|
|
376
434
|
>
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
435
|
+
{aside && diffTreeAvailable ? (
|
|
436
|
+
<Tabs
|
|
437
|
+
aria-label="Files listed"
|
|
438
|
+
value={asideView}
|
|
439
|
+
onValueChange={(value) => {
|
|
440
|
+
setRequestedAsideView(value as AsideView);
|
|
441
|
+
}}
|
|
442
|
+
>
|
|
443
|
+
{ordered ? listTab : null}
|
|
444
|
+
<Tab value="tree" label="Tree" />
|
|
445
|
+
{ordered ? null : listTab}
|
|
446
|
+
</Tabs>
|
|
447
|
+
) : null}
|
|
448
|
+
{aside && asideView === "tree" ? (
|
|
449
|
+
// a height of its own: the tree virtualizes, and grows to every row without one
|
|
450
|
+
<View className="web:h-[calc(100vh-12rem)]">
|
|
451
|
+
<Suspense fallback={<DiffLoading />}>
|
|
452
|
+
<PullRequestDiffTree
|
|
453
|
+
files={treeFiles}
|
|
454
|
+
viewedPaths={viewedPaths}
|
|
455
|
+
theme={theme}
|
|
456
|
+
onFilePress={goToFile}
|
|
457
|
+
/>
|
|
458
|
+
</Suspense>
|
|
459
|
+
</View>
|
|
460
|
+
) : (
|
|
461
|
+
<PullRequestDiffOverview
|
|
462
|
+
groups={shownDiff.groups}
|
|
463
|
+
omitted={shownDiff.omitted}
|
|
464
|
+
readingOrder={readingOrder.value}
|
|
465
|
+
readingOrderStatus={overviewStatus}
|
|
466
|
+
impacts={impacts.value}
|
|
467
|
+
impactsStatus={impactsStatus}
|
|
468
|
+
displayOrder={
|
|
469
|
+
(aside && diffTreeAvailable) || ordered
|
|
470
|
+
? readingOrder.value
|
|
471
|
+
: null
|
|
472
|
+
}
|
|
473
|
+
statuses={statuses}
|
|
474
|
+
viewedPaths={viewedPaths}
|
|
475
|
+
currentPath={fileInView}
|
|
476
|
+
aside={aside}
|
|
477
|
+
onFilePress={goToFile}
|
|
478
|
+
/>
|
|
479
|
+
)}
|
|
397
480
|
{/* the file header is pierre's shadow DOM, which no accent
|
|
398
481
|
class reaches: the color is read here and handed to it */}
|
|
399
482
|
<AccentScope accent="info">
|
|
@@ -418,6 +501,7 @@ export function PullRequestDiffView({
|
|
|
418
501
|
diffStyle={diffStyle}
|
|
419
502
|
theme={theme}
|
|
420
503
|
themeType={mode}
|
|
504
|
+
fontFamily={fontFamily}
|
|
421
505
|
wordWrap={wordWrap}
|
|
422
506
|
stickyTop={toolbarHeight}
|
|
423
507
|
collapse={collapse}
|