Mercurial
annotate hg-web/src/components/app.tsx @ 223:0e7b9464248d hg-web
[hg-web] Add browser route regression coverage
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 02 Aug 2026 10:07:40 -0700 |
| parents | ce7f4400c2de |
| children | 70de0c80d093 |
| rev | line source |
|---|---|
| 193 | 1 import React, { useState, useEffect, useCallback } from 'react'; |
| 2 import { Graph, useGraphData } from "hg-web/src/components/graph"; | |
| 3 import { DirectoryBrowser } from "hg-web/src/components/directory-browser"; | |
| 4 import { Header } from "hg-web/src/components/header"; | |
| 5 import { Footer } from "hg-web/src/components/footer"; | |
| 6 import { ThemeProvider, useTheme } from "hg-web/src/components/theme"; | |
| 7 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
8 type Page = 'landing' | 'graph' | 'directory' | 'changeset'; |
| 193 | 9 |
| 10 type RouteState = { | |
| 11 page: Page; | |
| 12 graphCommit?: string; | |
| 13 graphTip?: string; | |
| 14 dirPath?: string; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
15 changesetId?: string; |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
16 returnDepth?: number; |
| 193 | 17 } |
| 18 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
19 type ChangesetDetail = { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
20 node: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
21 date: [number, number]; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
22 desc: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
23 branch: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
24 bookmarks: string[]; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
25 tags: string[]; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
26 user: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
27 parents: string[]; |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
28 files: Array<{ |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
29 file: string; |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
30 status: string; |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
31 }>; |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
32 diff: Array<{ |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
33 blockno: number; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
34 lines: Array<{ t: string; n: number; l: string }>; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
35 }>; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
36 }; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
37 |
| 193 | 38 // Icons |
| 39 const ICONS = { | |
| 40 folder: "/icons/folder.png", | |
| 41 }; | |
| 42 | |
| 43 const GraphIcon = () => ( | |
| 44 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| 45 <circle cx="6" cy="6" r="3"/> | |
| 46 <circle cx="6" cy="18" r="3"/> | |
| 47 <circle cx="18" cy="12" r="3"/> | |
| 48 <line x1="6" y1="9" x2="6" y2="15"/> | |
| 49 <path d="M8.5 7.5L15.5 11"/> | |
| 50 </svg> | |
| 51 ); | |
| 52 | |
| 53 const FolderIcon = () => ( | |
| 54 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none"> | |
| 55 <path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/> | |
| 56 </svg> | |
| 57 ); | |
| 58 | |
| 59 const API_BASE = '/api/repo'; | |
| 60 | |
| 61 function parseRoute(): RouteState { | |
| 62 const params = new URLSearchParams(window.location.search); | |
| 63 const pathname = window.location.pathname; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
64 const changesetMatch = pathname.match(/^\/changeset\/([^/]+)$/); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
65 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
66 if (changesetMatch) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
67 try { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
68 const changesetId = decodeURIComponent(changesetMatch[1]); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
69 if (/^(?:[0-9a-f]{1,40}|tip)$/i.test(changesetId)) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
70 return { page: 'changeset', changesetId }; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
71 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
72 } catch { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
73 return { page: 'landing' }; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
74 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
75 } |
| 193 | 76 |
| 77 if (pathname.startsWith('/graph') || params.has('graph')) { | |
| 78 return { | |
| 79 page: 'graph', | |
| 80 graphCommit: params.get('commit') || undefined, | |
| 81 graphTip: params.get('tip') || undefined, | |
| 82 }; | |
| 83 } | |
| 84 | |
| 85 if (pathname.startsWith('/directory') || params.has('path')) { | |
| 86 return { | |
| 87 page: 'directory', | |
| 88 dirPath: params.get('path') || '', | |
| 89 }; | |
| 90 } | |
| 91 | |
| 92 return { page: 'landing' }; | |
| 93 } | |
| 94 | |
| 95 function buildUrl(state: RouteState): string { | |
| 96 const params = new URLSearchParams(); | |
| 97 | |
| 98 switch (state.page) { | |
| 99 case 'graph': | |
| 100 if (state.graphCommit) params.set('commit', state.graphCommit); | |
| 101 if (state.graphTip) params.set('tip', state.graphTip); | |
| 102 return `/graph${params.toString() ? '?' + params.toString() : ''}`; | |
| 103 case 'directory': | |
| 104 if (state.dirPath) params.set('path', state.dirPath); | |
| 105 return `/directory${params.toString() ? '?' + params.toString() : ''}`; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
106 case 'changeset': |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
107 return state.changesetId ? `/changeset/${encodeURIComponent(state.changesetId)}` : '/graph'; |
| 193 | 108 default: |
| 109 return '/'; | |
| 110 } | |
| 111 } | |
| 112 | |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
113 function isRouteState(value: unknown): value is RouteState { |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
114 if (!value || typeof value !== 'object' || !('page' in value)) return false; |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
115 return ['landing', 'graph', 'directory', 'changeset'].includes( |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
116 String((value as { page: unknown }).page), |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
117 ); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
118 } |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
119 |
| 193 | 120 // Landing Page Component |
| 121 function LandingPage({ | |
| 122 onNavigateToGraph, | |
| 123 onNavigateToDirectory, | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
124 onNavigateToChangeset, |
| 193 | 125 }: { |
| 126 onNavigateToGraph: () => void; | |
| 127 onNavigateToDirectory: (path?: string) => void; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
128 onNavigateToChangeset: (node: string) => void; |
| 193 | 129 }) { |
| 130 const [directories, setDirectories] = useState<any[]>([]); | |
| 131 const [files, setFiles] = useState<any[]>([]); | |
| 132 const [dirLoading, setDirLoading] = useState(true); | |
| 133 | |
| 134 const { data: graphData, loading: graphLoading } = useGraphData(); | |
| 135 | |
| 136 useEffect(() => { | |
| 137 fetch(`${API_BASE}/list`) | |
| 138 .then(r => r.json()) | |
| 139 .then(data => { | |
| 140 setDirectories(data.directories || []); | |
| 141 setFiles(data.files || []); | |
| 142 setDirLoading(false); | |
| 143 }) | |
| 144 .catch(() => setDirLoading(false)); | |
| 145 }, []); | |
| 146 | |
| 147 const previewItems = [ | |
| 148 ...directories.slice(0, 6), | |
| 149 ...files.slice(0, Math.max(0, 6 - directories.length)) | |
| 150 ].slice(0, 6); | |
| 151 | |
| 152 return ( | |
| 153 <div className="landing-grid"> | |
| 154 {/* Graph Preview */} | |
| 155 <div className="landing-section"> | |
| 156 <div className="landing-section-header"> | |
| 157 <span className="landing-section-title"> | |
| 158 <GraphIcon /> | |
| 159 Recent Commits | |
| 160 </span> | |
| 161 <a href="/graph" className="landing-section-link" onClick={(e) => { | |
| 162 e.preventDefault(); | |
| 163 onNavigateToGraph(); | |
| 164 }}> | |
| 165 View all | |
| 166 </a> | |
| 167 </div> | |
| 168 <div className="landing-section-content"> | |
| 169 {graphLoading ? ( | |
| 170 <div className="loading-state">Loading commits...</div> | |
| 171 ) : graphData ? ( | |
| 172 <Graph | |
| 173 data={graphData} | |
| 174 maxRows={8} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
175 onCommitClick={onNavigateToChangeset} |
| 193 | 176 /> |
| 177 ) : ( | |
| 178 <div className="empty-state">Failed to load commits</div> | |
| 179 )} | |
| 180 </div> | |
| 181 </div> | |
| 182 | |
| 183 {/* Directory Preview */} | |
| 184 <div className="landing-section"> | |
| 185 <div className="landing-section-header"> | |
| 186 <span className="landing-section-title"> | |
| 187 <FolderIcon /> | |
| 188 Repository Files | |
| 189 </span> | |
| 190 <a href="/directory" className="landing-section-link" onClick={(e) => { | |
| 191 e.preventDefault(); | |
| 192 onNavigateToDirectory(); | |
| 193 }}> | |
| 194 Browse all | |
| 195 </a> | |
| 196 </div> | |
| 197 <div className="landing-section-content"> | |
| 198 {dirLoading ? ( | |
| 199 <div className="loading-state">Loading files...</div> | |
| 200 ) : previewItems.length > 0 ? ( | |
| 201 previewItems.map((item) => ( | |
| 202 <div | |
| 203 key={item.abspath} | |
| 204 className="dir-item" | |
| 205 onClick={() => onNavigateToDirectory(item.abspath)} | |
| 206 > | |
| 207 <span className="dir-item-icon"> | |
| 208 <img | |
| 209 className="icon-invert" | |
| 210 src={directories.includes(item) ? ICONS.folder : "/icons/file.svg"} | |
| 211 alt="" | |
| 212 /> | |
| 213 </span> | |
| 214 <span className="dir-item-name">{item.basename}</span> | |
| 215 </div> | |
| 216 )) | |
| 217 ) : ( | |
| 218 <div className="empty-state">No files found</div> | |
| 219 )} | |
| 220 </div> | |
| 221 </div> | |
| 222 </div> | |
| 223 ); | |
| 224 } | |
| 225 | |
| 226 // Graph Page Component | |
| 227 function GraphPage({ | |
| 228 onBack, | |
| 229 initialCommit, | |
| 230 initialTip, | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
231 onOpenChangeset, |
| 193 | 232 }: { |
| 233 onBack: () => void; | |
| 234 initialCommit?: string; | |
| 235 initialTip?: string; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
236 onOpenChangeset: (node: string) => void; |
| 193 | 237 }) { |
| 238 const { data, loading, error, loadMore, hasMore, tip, currentCommit } = useGraphData({ | |
| 239 initialCommit: initialCommit || null, | |
| 240 graphTop: initialTip || null, | |
| 241 }); | |
| 242 | |
| 243 useEffect(() => { | |
| 244 if (tip && currentCommit) { | |
| 245 const params = new URLSearchParams(); | |
| 246 params.set('commit', currentCommit); | |
| 247 params.set('tip', tip); | |
| 248 const newUrl = `/graph?${params.toString()}`; | |
| 249 window.history.replaceState({ page: 'graph', graphCommit: currentCommit, graphTip: tip }, '', newUrl); | |
| 250 } | |
| 251 }, [currentCommit, tip]); | |
| 252 | |
| 253 return ( | |
| 254 <div> | |
| 255 <div className="page-header"> | |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
256 <button className="back-button" onClick={onBack} aria-label="Back"> |
| 193 | 257 ← Back |
| 258 </button> | |
| 259 <span className="page-title">Commit Graph</span> | |
| 260 </div> | |
| 261 | |
| 262 {tip && ( | |
| 263 <div className="graph-params"> | |
| 264 <span className="graph-param"> | |
| 265 <span className="graph-param-label">Tip:</span> | |
| 266 <span className="graph-param-value">{tip.substring(0, 12)}</span> | |
| 267 </span> | |
| 268 {currentCommit && currentCommit !== tip && ( | |
| 269 <span className="graph-param"> | |
| 270 <span className="graph-param-label">Current:</span> | |
| 271 <span className="graph-param-value">{currentCommit.substring(0, 12)}</span> | |
| 272 </span> | |
| 273 )} | |
| 274 </div> | |
| 275 )} | |
| 276 | |
| 277 {error && ( | |
| 278 <div className="error-message">Error: {error}</div> | |
| 279 )} | |
| 280 | |
| 281 <Graph | |
| 282 data={data} | |
| 283 loading={loading} | |
| 284 hasMore={hasMore} | |
| 285 onLoadMore={loadMore} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
286 onCommitClick={onOpenChangeset} |
| 193 | 287 /> |
| 288 </div> | |
| 289 ); | |
| 290 } | |
| 291 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
292 function ChangesetPage({ |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
293 changesetId, |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
294 onBack, |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
295 onOpenChangeset, |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
296 }: { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
297 changesetId: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
298 onBack: () => void; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
299 onOpenChangeset: (node: string) => void; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
300 }) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
301 const [changeset, setChangeset] = useState<ChangesetDetail | null>(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
302 const [loading, setLoading] = useState(true); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
303 const [error, setError] = useState<string | null>(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
304 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
305 useEffect(() => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
306 const controller = new AbortController(); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
307 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
308 setLoading(true); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
309 setError(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
310 setChangeset(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
311 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
312 fetch(`/api/changeset/${encodeURIComponent(changesetId)}`, { signal: controller.signal }) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
313 .then(async response => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
314 if (!response.ok) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
315 const message = await response.text(); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
316 throw new Error(message || `Unable to load changeset (${response.status})`); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
317 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
318 return response.json() as Promise<ChangesetDetail>; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
319 }) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
320 .then(setChangeset) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
321 .catch(err => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
322 if (err.name !== 'AbortError') setError(err.message); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
323 }) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
324 .finally(() => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
325 if (!controller.signal.aborted) setLoading(false); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
326 }); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
327 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
328 return () => controller.abort(); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
329 }, [changesetId]); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
330 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
331 return ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
332 <div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
333 <div className="page-header"> |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
334 <button className="back-button" onClick={onBack} aria-label="Back"> |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
335 ← Back |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
336 </button> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
337 <span className="page-title">Changeset</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
338 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
339 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
340 {loading && <div className="loading-state">Loading changeset...</div>} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
341 {error && <div className="error-message">Error: {error}</div>} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
342 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
343 {changeset && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
344 <article className="changeset-paper"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
345 <div className="changeset-heading"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
346 <code>{changeset.node}</code> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
347 <span className="changeset-branch">{changeset.branch}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
348 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
349 <h2>{changeset.desc}</h2> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
350 <div className="changeset-meta"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
351 <span>{changeset.user}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
352 <time dateTime={new Date(changeset.date[0] * 1000).toISOString()}> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
353 {new Date(changeset.date[0] * 1000).toLocaleString()} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
354 </time> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
355 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
356 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
357 {(changeset.bookmarks.length > 0 || changeset.tags.length > 0) && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
358 <div className="changeset-labels"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
359 {changeset.bookmarks.map(bookmark => <span key={`bookmark-${bookmark}`}>{bookmark}</span>)} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
360 {changeset.tags.map(tag => <span key={`tag-${tag}`}>{tag}</span>)} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
361 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
362 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
363 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
364 {changeset.parents.length > 0 && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
365 <div className="changeset-parents"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
366 <strong>Parents</strong> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
367 {changeset.parents.map(parent => ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
368 <button type="button" key={parent} onClick={() => onOpenChangeset(parent)}> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
369 {parent.substring(0, 12)} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
370 </button> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
371 ))} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
372 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
373 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
374 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
375 {changeset.files.length > 0 && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
376 <div className="changeset-files"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
377 <strong>Files</strong> |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
378 {changeset.files.map(file => ( |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
379 <code key={file.file}> |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
380 <span className={`changeset-file-status status-${file.status}`}> |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
381 {file.status} |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
382 </span> |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
383 {file.file} |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
384 </code> |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
385 ))} |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
386 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
387 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
388 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
389 <section className="changeset-diff" aria-label="Changeset diff"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
390 <h3>Diff</h3> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
391 {changeset.diff.length === 0 ? ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
392 <div className="empty-state">No textual changes in this changeset.</div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
393 ) : changeset.diff.map(block => ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
394 <pre key={block.blockno}> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
395 {block.lines.map((line, index) => ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
396 <span |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
397 key={`${line.n}-${index}`} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
398 className={ |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
399 line.t === '+' ? 'diff-add' : |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
400 line.t === '-' ? 'diff-remove' : |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
401 line.t === '@' ? 'diff-range' : 'diff-context' |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
402 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
403 > |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
404 <span className="diff-line-number">{line.n}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
405 <span>{line.l}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
406 </span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
407 ))} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
408 </pre> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
409 ))} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
410 </section> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
411 </article> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
412 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
413 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
414 ); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
415 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
416 |
| 193 | 417 // Directory Page Component |
| 418 function DirectoryPage({ | |
| 419 onBack, | |
| 420 initialPath, | |
| 421 onPathChange, | |
| 422 }: { | |
| 423 onBack: () => void; | |
| 424 initialPath?: string; | |
| 425 onPathChange: (path: string) => void; | |
| 426 }) { | |
| 427 return ( | |
| 428 <div> | |
| 429 <div className="page-header"> | |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
430 <button className="back-button" onClick={onBack} aria-label="Back"> |
| 193 | 431 ← Back |
| 432 </button> | |
| 433 <span className="page-title">Repository Files</span> | |
| 434 </div> | |
| 435 | |
| 436 <DirectoryBrowser | |
| 437 initialPath={initialPath} | |
| 438 onPathChange={onPathChange} | |
| 439 /> | |
| 440 </div> | |
| 441 ); | |
| 442 } | |
| 443 | |
| 444 // Main App Content (uses theme context) | |
| 445 function AppContent() { | |
| 446 const [route, setRoute] = useState<RouteState>(parseRoute); | |
| 447 const { isDark, toggleTheme } = useTheme(); | |
| 448 | |
| 449 // Handle browser back/forward | |
| 450 useEffect(() => { | |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
451 const handlePopState = (event: PopStateEvent) => { |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
452 setRoute(isRouteState(event.state) ? event.state : parseRoute()); |
| 193 | 453 }; |
| 454 window.addEventListener('popstate', handlePopState); | |
| 455 return () => window.removeEventListener('popstate', handlePopState); | |
| 456 }, []); | |
| 457 | |
| 458 const navigate = useCallback((newRoute: RouteState) => { | |
| 459 const url = buildUrl(newRoute); | |
| 460 window.history.pushState(newRoute, '', url); | |
| 461 setRoute(newRoute); | |
| 462 }, []); | |
| 463 | |
| 464 const navigateToLanding = useCallback(() => { | |
| 465 navigate({ page: 'landing' }); | |
| 466 }, [navigate]); | |
| 467 | |
| 468 const navigateToGraph = useCallback((commit?: string, tip?: string) => { | |
| 469 navigate({ page: 'graph', graphCommit: commit, graphTip: tip }); | |
| 470 }, [navigate]); | |
| 471 | |
| 472 const navigateToDirectory = useCallback((path?: string) => { | |
| 473 navigate({ page: 'directory', dirPath: path || '' }); | |
| 474 }, [navigate]); | |
| 475 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
476 const navigateToChangeset = useCallback((changesetId: string) => { |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
477 const returnDepth = route.page === 'changeset' |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
478 ? (route.returnDepth ?? 0) + 1 |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
479 : 1; |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
480 navigate({ page: 'changeset', changesetId, returnDepth }); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
481 }, [navigate, route.page, route.returnDepth]); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
482 |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
483 const navigateBackFromChangeset = useCallback(() => { |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
484 if (route.returnDepth !== undefined && route.returnDepth > 0) { |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
485 window.history.go(-route.returnDepth); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
486 return; |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
487 } |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
488 navigateToGraph(); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
489 }, [navigateToGraph, route.returnDepth]); |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
490 |
| 193 | 491 const handleDirectoryPathChange = useCallback((path: string) => { |
| 492 // Update URL without full navigation | |
| 493 const params = new URLSearchParams(); | |
| 494 if (path) params.set('path', path); | |
| 495 const newUrl = `/directory${params.toString() ? '?' + params.toString() : ''}`; | |
| 496 window.history.replaceState({ page: 'directory', dirPath: path }, '', newUrl); | |
| 497 setRoute(prev => ({ ...prev, dirPath: path })); | |
| 498 }, []); | |
| 499 | |
| 500 return ( | |
| 501 <div className="app-container"> | |
| 502 <Header | |
| 503 title="Zenbu Repository" | |
| 504 showThemeToggle={true} | |
| 505 isDark={isDark} | |
| 506 onToggleTheme={toggleTheme} | |
| 507 /> | |
| 508 | |
| 509 {/* Navigation Tabs */} | |
| 510 <div className="nav-tabs"> | |
| 511 <button | |
| 512 className={`nav-tab ${route.page === 'landing' ? 'active' : ''}`} | |
| 513 onClick={navigateToLanding} | |
| 514 > | |
| 515 Home | |
| 516 </button> | |
| 517 <button | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
518 className={`nav-tab ${route.page === 'graph' || route.page === 'changeset' ? 'active' : ''}`} |
| 193 | 519 onClick={() => navigateToGraph()} |
| 520 > | |
| 521 <GraphIcon /> | |
| 522 Graph | |
| 523 </button> | |
| 524 <button | |
| 525 className={`nav-tab ${route.page === 'directory' ? 'active' : ''}`} | |
| 526 onClick={() => navigateToDirectory()} | |
| 527 > | |
| 528 <FolderIcon /> | |
| 529 Files | |
| 530 </button> | |
| 531 </div> | |
| 532 | |
| 533 {/* Page Content */} | |
| 534 {route.page === 'landing' && ( | |
| 535 <LandingPage | |
| 536 onNavigateToGraph={() => navigateToGraph()} | |
| 537 onNavigateToDirectory={navigateToDirectory} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
538 onNavigateToChangeset={navigateToChangeset} |
| 193 | 539 /> |
| 540 )} | |
| 541 | |
| 542 {route.page === 'graph' && ( | |
| 543 <GraphPage | |
| 544 onBack={navigateToLanding} | |
| 545 initialCommit={route.graphCommit} | |
| 546 initialTip={route.graphTip} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
547 onOpenChangeset={navigateToChangeset} |
| 193 | 548 /> |
| 549 )} | |
| 550 | |
| 551 {route.page === 'directory' && ( | |
| 552 <DirectoryPage | |
| 553 onBack={navigateToLanding} | |
| 554 initialPath={route.dirPath} | |
| 555 onPathChange={handleDirectoryPathChange} | |
| 556 /> | |
| 557 )} | |
| 558 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
559 {route.page === 'changeset' && route.changesetId && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
560 <ChangesetPage |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
561 changesetId={route.changesetId} |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
562 onBack={navigateBackFromChangeset} |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
563 onOpenChangeset={navigateToChangeset} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
564 /> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
565 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
566 |
| 193 | 567 <Footer /> |
| 568 </div> | |
| 569 ); | |
| 570 } | |
| 571 | |
| 572 // App wrapper with ThemeProvider | |
| 573 function App() { | |
| 574 return ( | |
| 575 <ThemeProvider> | |
| 576 <AppContent /> | |
| 577 </ThemeProvider> | |
| 578 ); | |
| 579 } | |
| 580 | |
| 581 export { App }; |