Skip to content
File

Blob: src/client/islands/merge-expander.tsx

typescript170 lines
1/// <reference lib="dom" />
2 
3import type { MouseEvent } from "react";
4import { Fragment, useState } from "react";
5 
6import { hydrateIsland } from "@/client/hydrate";
7import { CommitRow } from "@/client/components/CommitRow";
8 
9type CommitView = {
10 oid: string;
11 shortOid: string;
12 firstLine: string;
13 authorName: string;
14 when: string;
15 isMerge?: boolean;
16};
17 
18export type MergeExpanderProps = {
19 owner: string;
20 repo: string;
21 commits: CommitView[];
22};
23 
24function MergeStatusRow({ message }: { message: string }) {
25 return (
26 <tr>
27 <td colSpan={4} className="text-zinc-500 dark:text-zinc-400">
28 {message}
29 </td>
30 </tr>
31 );
32}
33 
34export function MergeExpanderIsland({ owner, repo, commits }: MergeExpanderProps) {
35 const [expandedByOid, setExpandedByOid] = useState<Record<string, boolean>>({});
36 const [loadingByOid, setLoadingByOid] = useState<Record<string, boolean>>({});
37 const [errorByOid, setErrorByOid] = useState<Record<string, string | null>>({});
38 const [mergeRowsByOid, setMergeRowsByOid] = useState<Record<string, CommitView[]>>({});
39 
40 async function toggleMerge(oid: string) {
41 if (loadingByOid[oid]) {
42 return;
43 }
44 
45 if (expandedByOid[oid]) {
46 setExpandedByOid((current) => ({ ...current, [oid]: false }));
47 return;
48 }
49 
50 if (mergeRowsByOid[oid]) {
51 setExpandedByOid((current) => ({ ...current, [oid]: true }));
52 setErrorByOid((current) => ({ ...current, [oid]: null }));
53 return;
54 }
55 
56 setLoadingByOid((current) => ({ ...current, [oid]: true }));
57 setExpandedByOid((current) => ({ ...current, [oid]: true }));
58 setErrorByOid((current) => ({ ...current, [oid]: null }));
59 
60 try {
61 const response = await fetch(`/${owner}/${repo}/commits/fragments/${oid}?limit=20`, {
62 headers: { Accept: "application/json" },
63 });
64 if (!response.ok) {
65 throw new Error(`HTTP ${response.status}`);
66 }
67 
68 const data = (await response.json()) as { commits?: CommitView[] };
69 setMergeRowsByOid((current) => ({ ...current, [oid]: data.commits || [] }));
70 } catch (error) {
71 setErrorByOid((current) => ({
72 ...current,
73 [oid]: error instanceof Error ? error.message : "Failed to load merge commits",
74 }));
75 } finally {
76 setLoadingByOid((current) => ({ ...current, [oid]: false }));
77 }
78 }
79 
80 function onMergeRowClick(oid: string) {
81 return (event: MouseEvent<HTMLTableRowElement>) => {
82 const target = event.target as HTMLElement;
83 if (target.closest("a, button")) {
84 return;
85 }
86 
87 void toggleMerge(oid);
88 };
89 }
90 
91 return (
92 <table
93 id="commits-table"
94 data-owner={owner}
95 data-repo={repo}
96 className="mt-4 overflow-hidden rounded-2xl border border-zinc-200 shadow-xs dark:border-zinc-800/60"
97 >
98 <thead>
99 <tr>
100 <th>OID</th>
101 <th>Message</th>
102 <th>Author</th>
103 <th>Date</th>
104 </tr>
105 </thead>
106 <tbody>
107 {commits.length ? (
108 commits.map((commit) => {
109 const isMerge = Boolean(commit.isMerge);
110 const mergeOid = commit.oid;
111 const isExpanded = Boolean(expandedByOid[mergeOid]);
112 const isLoading = Boolean(loadingByOid[mergeOid]);
113 const mergeRows = mergeRowsByOid[mergeOid] || [];
114 const error = errorByOid[mergeOid];
115 const mergeRowClass = isMerge
116 ? "cursor-pointer bg-accent-50/40 hover:bg-accent-50/60 dark:bg-accent-900/15 dark:hover:bg-accent-900/25"
117 : "";
118
119 return (
120 <Fragment key={commit.oid}>
121 <CommitRow
122 owner={owner}
123 repo={repo}
124 commit={commit}
125 isMerge={isMerge}
126 rowClass={mergeRowClass}
127 toggleOid={isMerge ? mergeOid : undefined}
128 mergeExpanded={isExpanded}
129 onToggle={isMerge ? onMergeRowClick(mergeOid) : undefined}
130 />
131 {isMerge && isExpanded && isLoading ? (
132 <MergeStatusRow message="Loading..." />
133 ) : null}
134 {isMerge && isExpanded && !isLoading && error ? (
135 <MergeStatusRow message={`Failed to load merge commits: ${error}`} />
136 ) : null}
137 {isMerge && isExpanded && !isLoading && !error && !mergeRows.length ? (
138 <MergeStatusRow message="(No commits to show for this merge yet)" />
139 ) : null}
140 {isMerge && isExpanded && !isLoading && !error
141 ? mergeRows.map((entry) => (
142 <CommitRow
143 key={entry.oid}
144 owner={owner}
145 repo={repo}
146 commit={entry}
147 compact
148 mergeOf={mergeOid}
149 />
150 ))
151 : null}
152 </Fragment>
153 );
154 })
155 ) : (
156 <tr>
157 <td colSpan={4} className="text-zinc-500 dark:text-zinc-400">
158 (none)
159 </td>
160 </tr>
161 )}
162 </tbody>
163 </table>
164 );
165}
166 
167export function initMergeExpander() {
168 hydrateIsland<MergeExpanderProps>("merge-expander", MergeExpanderIsland);
169}