Repository navigation
Expand file tree
/
Copy pathusePullRequestDiff.ts
More file actions
155 lines (147 loc) · 5.05 KB
/
Copy pathusePullRequestDiff.ts
File metadata and controls
155 lines (147 loc) · 5.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import type { ChangeRequestTerminology } from "@t3tools/client-runtime/source-control-clients";
import {
EnvironmentId,
ProjectId,
type PullRequestListEntry,
type PullRequestSummary,
} from "@t3tools/contracts";
import { useAtomRefresh } from "@effect/atom-react";
import { useLayoutEffect, useMemo, useState } from "react";
import { composerPullRequests } from "../../state/pull-requests";
import { useEnvironmentQuery } from "../../state/query";
import { usePullToRefresh } from "../../state/use-pull-to-refresh";
import {
getCachedPullRequestDiffPages,
retainPullRequestDiffPage,
type PullRequestDiffPage,
} from "./pullRequestDiffPages";
export type PullRequestParams = {
readonly environmentId: string;
readonly projectId: string;
readonly repository: string;
readonly number: number;
readonly host?: string;
readonly navigationUrl?: string;
readonly navigationId?: number;
readonly initialSummary?: PullRequestListEntry | PullRequestSummary;
};
export type PullRequestDiffParams = PullRequestParams & { readonly commit?: string };
export function pullRequestTarget(params: PullRequestParams) {
return {
environmentId: EnvironmentId.make(params.environmentId),
input: {
projectId: ProjectId.make(params.projectId),
repository: params.repository,
number: params.number,
...(params.host ? { host: params.host } : {}),
},
};
}
const NO_PAGES: readonly PullRequestDiffPage[] = [];
export function usePullRequestDiffEntry(navigation: {
readonly addListener: (
type: "transitionStart" | "transitionEnd",
listener: (event: { readonly data: { readonly closing: boolean } }) => void,
) => () => void;
}) {
const [ready, setReady] = useState(false);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- subscribes to navigator transition events and frames to defer the diff until the opening transition ends
useLayoutEffect(() => {
if (ready) return;
let opening = false;
let closing = false;
const finish = () => {
if (!closing) setReady(true);
};
const removeStart = navigation.addListener("transitionStart", ({ data }) => {
closing = data.closing;
if (!closing) opening = true;
});
const removeEnd = navigation.addListener("transitionEnd", ({ data }) => {
closing = data.closing;
if (!closing) finish();
});
// Initial and unanimated presentations may have no transition events; let the shell paint first.
let frame = requestAnimationFrame(() => {
frame = requestAnimationFrame(() => {
if (!opening) finish();
});
});
// A missing native completion must not strand the route on its loading state.
const fallback = setTimeout(finish, 1_000);
return () => {
removeStart();
removeEnd();
cancelAnimationFrame(frame);
clearTimeout(fallback);
};
}, [navigation, ready]);
return ready;
}
export function usePullRequestDiff(
params: PullRequestDiffParams,
terminology: ChangeRequestTerminology,
) {
const reference = pullRequestTarget(params);
const target = {
...reference,
input: { ...reference.input, ...(params.commit ? { commit: params.commit } : {}) },
};
const key = JSON.stringify(target);
const firstPageAtom = composerPullRequests.diff(target);
const refreshFirstPage = useAtomRefresh(firstPageAtom);
const { refreshing, onRefresh } = usePullToRefresh(firstPageAtom);
const [state, setState] = useState<{
key: string;
cursor: string | null;
pages: readonly PullRequestDiffPage[];
}>({ key, cursor: null, pages: NO_PAGES });
const cursor = state.key === key ? state.cursor : null;
const query = useEnvironmentQuery(
composerPullRequests.diff({
...target,
input: { ...target.input, ...(cursor ? { cursor } : {}) },
}),
);
const pages = useMemo(() => {
const loaded = state.key === key ? state.pages : NO_PAGES;
return query.data ? retainPullRequestDiffPage(loaded, cursor, query.data) : loaded;
}, [cursor, key, query.data, state.key, state.pages]);
if (state.key !== key || state.pages !== pages) setState({ key, cursor, pages });
const parsed = useMemo(
() => getCachedPullRequestDiffPages(pages, key, terminology),
[key, pages, terminology],
);
const patch = useMemo(() => pages.map((page) => page.result.patch).join("\n"), [pages]);
const nextCursor = pages.at(-1)?.result.nextCursor ?? null;
const canLoadMore =
nextCursor !== null && nextCursor !== cursor && !query.isPending && query.error === null;
const loadMore = () => {
if (!canLoadMore) return;
setState({ key, cursor: nextCursor, pages });
};
const { error, refresh: refreshPage } = query;
const refresh = () => {
if (cursor !== null && error !== null) {
refreshPage();
return;
}
setState({ key, cursor: null, pages: NO_PAGES });
refreshFirstPage();
};
const onPullRefresh = () => {
setState({ key, cursor: null, pages: NO_PAGES });
onRefresh();
};
return {
...query,
refresh,
parsed,
patch,
nextCursor,
canLoadMore,
loadMore,
refreshing,
onPullRefresh,
};
}