Mercurial
annotate hg-web/src/components/app.tsx @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | 70de0c80d093 |
| children |
| 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 |
|
225
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
38 type DiffLine = ChangesetDetail['diff'][number]['lines'][number]; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
39 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
40 type DiffCell = { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
41 lineNumber: number | null; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
42 text: string; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
43 kind: 'context' | 'add' | 'remove' | 'meta'; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
44 }; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
45 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
46 type SideBySideRow = { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
47 left?: DiffCell; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
48 right?: DiffCell; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
49 range?: string; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
50 }; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
51 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
52 function trimDiffLine(line: string): string { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
53 return line.endsWith('\n') ? line.slice(0, -1) : line; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
54 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
55 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
56 function contentDiffLine(line: DiffLine): string { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
57 const text = trimDiffLine(line.l); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
58 if ((line.t === '+' || line.t === '-' || line.t === '' || line.t === ' ') && |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
59 text.startsWith(line.t || ' ')) { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
60 return text.slice(1); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
61 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
62 return text; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
63 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
64 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
65 function buildSideBySideRows(lines: DiffLine[]): SideBySideRow[] { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
66 const rows: SideBySideRow[] = []; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
67 let removals: DiffCell[] = []; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
68 let additions: DiffCell[] = []; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
69 let oldLine: number | null = null; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
70 let newLine: number | null = null; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
71 let inHunk = false; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
72 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
73 const flushChanges = () => { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
74 const count = Math.max(removals.length, additions.length); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
75 for (let index = 0; index < count; index++) { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
76 rows.push({ left: removals[index], right: additions[index] }); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
77 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
78 removals = []; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
79 additions = []; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
80 }; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
81 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
82 for (const line of lines) { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
83 if (line.t === '@') { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
84 flushChanges(); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
85 const range = trimDiffLine(line.l); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
86 const match = range.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
87 oldLine = match ? Number(match[1]) : null; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
88 newLine = match ? Number(match[2]) : null; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
89 inHunk = true; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
90 rows.push({ range }); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
91 continue; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
92 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
93 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
94 if (!inHunk && (line.t === '-' || line.t === '+')) { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
95 const cell: DiffCell = { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
96 lineNumber: null, |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
97 text: trimDiffLine(line.l), |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
98 kind: 'meta', |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
99 }; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
100 if (line.t === '-') removals.push(cell); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
101 else additions.push(cell); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
102 continue; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
103 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
104 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
105 if (line.t === '-') { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
106 removals.push({ |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
107 lineNumber: oldLine, |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
108 text: contentDiffLine(line), |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
109 kind: 'remove', |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
110 }); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
111 if (oldLine !== null) oldLine++; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
112 continue; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
113 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
114 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
115 if (line.t === '+') { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
116 additions.push({ |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
117 lineNumber: newLine, |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
118 text: contentDiffLine(line), |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
119 kind: 'add', |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
120 }); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
121 if (newLine !== null) newLine++; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
122 continue; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
123 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
124 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
125 flushChanges(); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
126 const text = contentDiffLine(line); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
127 rows.push({ |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
128 left: { lineNumber: oldLine, text, kind: 'context' }, |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
129 right: { lineNumber: newLine, text, kind: 'context' }, |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
130 }); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
131 if (oldLine !== null) oldLine++; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
132 if (newLine !== null) newLine++; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
133 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
134 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
135 flushChanges(); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
136 return rows; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
137 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
138 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
139 function diffBlockFilename( |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
140 block: ChangesetDetail['diff'][number], |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
141 fallback?: string, |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
142 ): string { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
143 if (fallback) return fallback; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
144 const newFileHeader = block.lines.find( |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
145 line => line.t === '+' && line.l.startsWith('+++ '), |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
146 ); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
147 if (!newFileHeader) return `Diff block ${block.blockno}`; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
148 return trimDiffLine(newFileHeader.l).replace(/^\+\+\+ (?:b\/)?/, '').split('\t')[0]; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
149 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
150 |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
151 function SideBySideDiff({ |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
152 block, |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
153 filename, |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
154 }: { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
155 block: ChangesetDetail['diff'][number]; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
156 filename: string; |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
157 }) { |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
158 const rows = buildSideBySideRows(block.lines); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
159 return ( |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
160 <div className="side-by-side-diff"> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
161 <div className="diff-file-header">{filename}</div> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
162 <div className="diff-column-headings"> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
163 <span>Before</span> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
164 <span>After</span> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
165 </div> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
166 <div className="diff-grid"> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
167 {rows.map((row, index) => ( |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
168 row.range ? ( |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
169 <div className="diff-range-row" key={`range-${index}`}>{row.range}</div> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
170 ) : ( |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
171 <React.Fragment key={`row-${index}`}> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
172 <span className={`diff-side-number diff-${row.left?.kind || 'empty'}`}> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
173 {row.left?.lineNumber ?? ''} |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
174 </span> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
175 <code className={`diff-side-code diff-left diff-${row.left?.kind || 'empty'}`}> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
176 {row.left?.text ?? ''} |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
177 </code> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
178 <span className={`diff-side-number diff-column-divider diff-${row.right?.kind || 'empty'}`}> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
179 {row.right?.lineNumber ?? ''} |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
180 </span> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
181 <code className={`diff-side-code diff-right diff-${row.right?.kind || 'empty'}`}> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
182 {row.right?.text ?? ''} |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
183 </code> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
184 </React.Fragment> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
185 ) |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
186 ))} |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
187 </div> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
188 </div> |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
189 ); |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
190 } |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
191 |
| 193 | 192 // Icons |
| 193 const ICONS = { | |
| 194 folder: "/icons/folder.png", | |
| 195 }; | |
| 196 | |
| 197 const GraphIcon = () => ( | |
| 198 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| 199 <circle cx="6" cy="6" r="3"/> | |
| 200 <circle cx="6" cy="18" r="3"/> | |
| 201 <circle cx="18" cy="12" r="3"/> | |
| 202 <line x1="6" y1="9" x2="6" y2="15"/> | |
| 203 <path d="M8.5 7.5L15.5 11"/> | |
| 204 </svg> | |
| 205 ); | |
| 206 | |
| 207 const FolderIcon = () => ( | |
| 208 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none"> | |
| 209 <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"/> | |
| 210 </svg> | |
| 211 ); | |
| 212 | |
| 213 const API_BASE = '/api/repo'; | |
| 214 | |
| 215 function parseRoute(): RouteState { | |
| 216 const params = new URLSearchParams(window.location.search); | |
| 217 const pathname = window.location.pathname; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
218 const changesetMatch = pathname.match(/^\/changeset\/([^/]+)$/); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
219 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
220 if (changesetMatch) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
221 try { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
222 const changesetId = decodeURIComponent(changesetMatch[1]); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
223 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
|
224 return { page: 'changeset', changesetId }; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
225 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
226 } catch { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
227 return { page: 'landing' }; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
228 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
229 } |
| 193 | 230 |
| 231 if (pathname.startsWith('/graph') || params.has('graph')) { | |
| 232 return { | |
| 233 page: 'graph', | |
| 234 graphCommit: params.get('commit') || undefined, | |
| 235 graphTip: params.get('tip') || undefined, | |
| 236 }; | |
| 237 } | |
| 238 | |
| 239 if (pathname.startsWith('/directory') || params.has('path')) { | |
| 240 return { | |
| 241 page: 'directory', | |
| 242 dirPath: params.get('path') || '', | |
| 243 }; | |
| 244 } | |
| 245 | |
| 246 return { page: 'landing' }; | |
| 247 } | |
| 248 | |
| 249 function buildUrl(state: RouteState): string { | |
| 250 const params = new URLSearchParams(); | |
| 251 | |
| 252 switch (state.page) { | |
| 253 case 'graph': | |
| 254 if (state.graphCommit) params.set('commit', state.graphCommit); | |
| 255 if (state.graphTip) params.set('tip', state.graphTip); | |
| 256 return `/graph${params.toString() ? '?' + params.toString() : ''}`; | |
| 257 case 'directory': | |
| 258 if (state.dirPath) params.set('path', state.dirPath); | |
| 259 return `/directory${params.toString() ? '?' + params.toString() : ''}`; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
260 case 'changeset': |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
261 return state.changesetId ? `/changeset/${encodeURIComponent(state.changesetId)}` : '/graph'; |
| 193 | 262 default: |
| 263 return '/'; | |
| 264 } | |
| 265 } | |
| 266 | |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
267 function isRouteState(value: unknown): value is RouteState { |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
268 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
|
269 return ['landing', 'graph', 'directory', 'changeset'].includes( |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
270 String((value as { page: unknown }).page), |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
271 ); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
272 } |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
273 |
| 193 | 274 // Landing Page Component |
| 275 function LandingPage({ | |
| 276 onNavigateToGraph, | |
| 277 onNavigateToDirectory, | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
278 onNavigateToChangeset, |
| 193 | 279 }: { |
| 280 onNavigateToGraph: () => void; | |
| 281 onNavigateToDirectory: (path?: string) => void; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
282 onNavigateToChangeset: (node: string) => void; |
| 193 | 283 }) { |
| 284 const [directories, setDirectories] = useState<any[]>([]); | |
| 285 const [files, setFiles] = useState<any[]>([]); | |
| 286 const [dirLoading, setDirLoading] = useState(true); | |
| 287 | |
| 288 const { data: graphData, loading: graphLoading } = useGraphData(); | |
| 289 | |
| 290 useEffect(() => { | |
| 291 fetch(`${API_BASE}/list`) | |
| 292 .then(r => r.json()) | |
| 293 .then(data => { | |
| 294 setDirectories(data.directories || []); | |
| 295 setFiles(data.files || []); | |
| 296 setDirLoading(false); | |
| 297 }) | |
| 298 .catch(() => setDirLoading(false)); | |
| 299 }, []); | |
| 300 | |
| 301 const previewItems = [ | |
| 302 ...directories.slice(0, 6), | |
| 303 ...files.slice(0, Math.max(0, 6 - directories.length)) | |
| 304 ].slice(0, 6); | |
| 305 | |
| 306 return ( | |
| 307 <div className="landing-grid"> | |
| 308 {/* Graph Preview */} | |
| 309 <div className="landing-section"> | |
| 310 <div className="landing-section-header"> | |
| 311 <span className="landing-section-title"> | |
| 312 <GraphIcon /> | |
| 313 Recent Commits | |
| 314 </span> | |
| 315 <a href="/graph" className="landing-section-link" onClick={(e) => { | |
| 316 e.preventDefault(); | |
| 317 onNavigateToGraph(); | |
| 318 }}> | |
| 319 View all | |
| 320 </a> | |
| 321 </div> | |
| 322 <div className="landing-section-content"> | |
| 323 {graphLoading ? ( | |
| 324 <div className="loading-state">Loading commits...</div> | |
| 325 ) : graphData ? ( | |
| 326 <Graph | |
| 327 data={graphData} | |
| 328 maxRows={8} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
329 onCommitClick={onNavigateToChangeset} |
| 193 | 330 /> |
| 331 ) : ( | |
| 332 <div className="empty-state">Failed to load commits</div> | |
| 333 )} | |
| 334 </div> | |
| 335 </div> | |
| 336 | |
| 337 {/* Directory Preview */} | |
| 338 <div className="landing-section"> | |
| 339 <div className="landing-section-header"> | |
| 340 <span className="landing-section-title"> | |
| 341 <FolderIcon /> | |
| 342 Repository Files | |
| 343 </span> | |
| 344 <a href="/directory" className="landing-section-link" onClick={(e) => { | |
| 345 e.preventDefault(); | |
| 346 onNavigateToDirectory(); | |
| 347 }}> | |
| 348 Browse all | |
| 349 </a> | |
| 350 </div> | |
| 351 <div className="landing-section-content"> | |
| 352 {dirLoading ? ( | |
| 353 <div className="loading-state">Loading files...</div> | |
| 354 ) : previewItems.length > 0 ? ( | |
| 355 previewItems.map((item) => ( | |
| 356 <div | |
| 357 key={item.abspath} | |
| 358 className="dir-item" | |
| 359 onClick={() => onNavigateToDirectory(item.abspath)} | |
| 360 > | |
| 361 <span className="dir-item-icon"> | |
| 362 <img | |
| 363 className="icon-invert" | |
| 364 src={directories.includes(item) ? ICONS.folder : "/icons/file.svg"} | |
| 365 alt="" | |
| 366 /> | |
| 367 </span> | |
| 368 <span className="dir-item-name">{item.basename}</span> | |
| 369 </div> | |
| 370 )) | |
| 371 ) : ( | |
| 372 <div className="empty-state">No files found</div> | |
| 373 )} | |
| 374 </div> | |
| 375 </div> | |
| 376 </div> | |
| 377 ); | |
| 378 } | |
| 379 | |
| 380 // Graph Page Component | |
| 381 function GraphPage({ | |
| 382 onBack, | |
| 383 initialCommit, | |
| 384 initialTip, | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
385 onOpenChangeset, |
| 193 | 386 }: { |
| 387 onBack: () => void; | |
| 388 initialCommit?: string; | |
| 389 initialTip?: string; | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
390 onOpenChangeset: (node: string) => void; |
| 193 | 391 }) { |
| 392 const { data, loading, error, loadMore, hasMore, tip, currentCommit } = useGraphData({ | |
| 393 initialCommit: initialCommit || null, | |
| 394 graphTop: initialTip || null, | |
| 395 }); | |
| 396 | |
| 397 useEffect(() => { | |
| 398 if (tip && currentCommit) { | |
| 399 const params = new URLSearchParams(); | |
| 400 params.set('commit', currentCommit); | |
| 401 params.set('tip', tip); | |
| 402 const newUrl = `/graph?${params.toString()}`; | |
| 403 window.history.replaceState({ page: 'graph', graphCommit: currentCommit, graphTip: tip }, '', newUrl); | |
| 404 } | |
| 405 }, [currentCommit, tip]); | |
| 406 | |
| 407 return ( | |
| 408 <div> | |
| 409 <div className="page-header"> | |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
410 <button className="back-button" onClick={onBack} aria-label="Back"> |
| 193 | 411 ← Back |
| 412 </button> | |
| 413 <span className="page-title">Commit Graph</span> | |
| 414 </div> | |
| 415 | |
| 416 {tip && ( | |
| 417 <div className="graph-params"> | |
| 418 <span className="graph-param"> | |
| 419 <span className="graph-param-label">Tip:</span> | |
| 420 <span className="graph-param-value">{tip.substring(0, 12)}</span> | |
| 421 </span> | |
| 422 {currentCommit && currentCommit !== tip && ( | |
| 423 <span className="graph-param"> | |
| 424 <span className="graph-param-label">Current:</span> | |
| 425 <span className="graph-param-value">{currentCommit.substring(0, 12)}</span> | |
| 426 </span> | |
| 427 )} | |
| 428 </div> | |
| 429 )} | |
| 430 | |
| 431 {error && ( | |
| 432 <div className="error-message">Error: {error}</div> | |
| 433 )} | |
| 434 | |
| 435 <Graph | |
| 436 data={data} | |
| 437 loading={loading} | |
| 438 hasMore={hasMore} | |
| 439 onLoadMore={loadMore} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
440 onCommitClick={onOpenChangeset} |
| 193 | 441 /> |
| 442 </div> | |
| 443 ); | |
| 444 } | |
| 445 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
446 function ChangesetPage({ |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
447 changesetId, |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
448 onBack, |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
449 onOpenChangeset, |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
450 }: { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
451 changesetId: string; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
452 onBack: () => void; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
453 onOpenChangeset: (node: string) => void; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
454 }) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
455 const [changeset, setChangeset] = useState<ChangesetDetail | null>(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
456 const [loading, setLoading] = useState(true); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
457 const [error, setError] = useState<string | null>(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
458 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
459 useEffect(() => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
460 const controller = new AbortController(); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
461 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
462 setLoading(true); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
463 setError(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
464 setChangeset(null); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
465 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
466 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
|
467 .then(async response => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
468 if (!response.ok) { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
469 const message = await response.text(); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
470 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
|
471 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
472 return response.json() as Promise<ChangesetDetail>; |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
473 }) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
474 .then(setChangeset) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
475 .catch(err => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
476 if (err.name !== 'AbortError') setError(err.message); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
477 }) |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
478 .finally(() => { |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
479 if (!controller.signal.aborted) setLoading(false); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
480 }); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
481 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
482 return () => controller.abort(); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
483 }, [changesetId]); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
484 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
485 return ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
486 <div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
487 <div className="page-header"> |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
488 <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
|
489 ← Back |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
490 </button> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
491 <span className="page-title">Changeset</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
492 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
493 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
494 {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
|
495 {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
|
496 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
497 {changeset && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
498 <article className="changeset-paper"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
499 <div className="changeset-heading"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
500 <code>{changeset.node}</code> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
501 <span className="changeset-branch">{changeset.branch}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
502 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
503 <h2>{changeset.desc}</h2> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
504 <div className="changeset-meta"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
505 <span>{changeset.user}</span> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
506 <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
|
507 {new Date(changeset.date[0] * 1000).toLocaleString()} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
508 </time> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
509 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
510 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
511 {(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
|
512 <div className="changeset-labels"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
513 {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
|
514 {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
|
515 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
516 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
517 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
518 {changeset.parents.length > 0 && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
519 <div className="changeset-parents"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
520 <strong>Parents</strong> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
521 {changeset.parents.map(parent => ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
522 <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
|
523 {parent.substring(0, 12)} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
524 </button> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
525 ))} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
526 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
527 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
528 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
529 {changeset.files.length > 0 && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
530 <div className="changeset-files"> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
531 <strong>Files</strong> |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
532 {changeset.files.map(file => ( |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
533 <code key={file.file}> |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
534 <span className={`changeset-file-status status-${file.status}`}> |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
535 {file.status} |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
536 </span> |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
537 {file.file} |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
538 </code> |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
539 ))} |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
540 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
541 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
542 |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
543 <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
|
544 <h3>Diff</h3> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
545 {changeset.diff.length === 0 ? ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
546 <div className="empty-state">No textual changes in this changeset.</div> |
|
225
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
547 ) : changeset.diff.map((block, index) => ( |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
548 <SideBySideDiff |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
549 key={block.blockno} |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
550 block={block} |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
551 filename={diffBlockFilename(block, changeset.files[index]?.file)} |
|
70de0c80d093
[hg-web] Add side-by-side changeset diffs
MrJuneJune <me@mrjunejune.com>
parents:
223
diff
changeset
|
552 /> |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
553 ))} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
554 </section> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
555 </article> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
556 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
557 </div> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
558 ); |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
559 } |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
560 |
| 193 | 561 // Directory Page Component |
| 562 function DirectoryPage({ | |
| 563 onBack, | |
| 564 initialPath, | |
| 565 onPathChange, | |
| 566 }: { | |
| 567 onBack: () => void; | |
| 568 initialPath?: string; | |
| 569 onPathChange: (path: string) => void; | |
| 570 }) { | |
| 571 return ( | |
| 572 <div> | |
| 573 <div className="page-header"> | |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
574 <button className="back-button" onClick={onBack} aria-label="Back"> |
| 193 | 575 ← Back |
| 576 </button> | |
| 577 <span className="page-title">Repository Files</span> | |
| 578 </div> | |
| 579 | |
| 580 <DirectoryBrowser | |
| 581 initialPath={initialPath} | |
| 582 onPathChange={onPathChange} | |
| 583 /> | |
| 584 </div> | |
| 585 ); | |
| 586 } | |
| 587 | |
| 588 // Main App Content (uses theme context) | |
| 589 function AppContent() { | |
| 590 const [route, setRoute] = useState<RouteState>(parseRoute); | |
| 591 const { isDark, toggleTheme } = useTheme(); | |
| 592 | |
| 593 // Handle browser back/forward | |
| 594 useEffect(() => { | |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
595 const handlePopState = (event: PopStateEvent) => { |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
596 setRoute(isRouteState(event.state) ? event.state : parseRoute()); |
| 193 | 597 }; |
| 598 window.addEventListener('popstate', handlePopState); | |
| 599 return () => window.removeEventListener('popstate', handlePopState); | |
| 600 }, []); | |
| 601 | |
| 602 const navigate = useCallback((newRoute: RouteState) => { | |
| 603 const url = buildUrl(newRoute); | |
| 604 window.history.pushState(newRoute, '', url); | |
| 605 setRoute(newRoute); | |
| 606 }, []); | |
| 607 | |
| 608 const navigateToLanding = useCallback(() => { | |
| 609 navigate({ page: 'landing' }); | |
| 610 }, [navigate]); | |
| 611 | |
| 612 const navigateToGraph = useCallback((commit?: string, tip?: string) => { | |
| 613 navigate({ page: 'graph', graphCommit: commit, graphTip: tip }); | |
| 614 }, [navigate]); | |
| 615 | |
| 616 const navigateToDirectory = useCallback((path?: string) => { | |
| 617 navigate({ page: 'directory', dirPath: path || '' }); | |
| 618 }, [navigate]); | |
| 619 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
620 const navigateToChangeset = useCallback((changesetId: string) => { |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
621 const returnDepth = route.page === 'changeset' |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
622 ? (route.returnDepth ?? 0) + 1 |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
623 : 1; |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
624 navigate({ page: 'changeset', changesetId, returnDepth }); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
625 }, [navigate, route.page, route.returnDepth]); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
626 |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
627 const navigateBackFromChangeset = useCallback(() => { |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
628 if (route.returnDepth !== undefined && route.returnDepth > 0) { |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
629 window.history.go(-route.returnDepth); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
630 return; |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
631 } |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
632 navigateToGraph(); |
|
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
633 }, [navigateToGraph, route.returnDepth]); |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
634 |
| 193 | 635 const handleDirectoryPathChange = useCallback((path: string) => { |
| 636 // Update URL without full navigation | |
| 637 const params = new URLSearchParams(); | |
| 638 if (path) params.set('path', path); | |
| 639 const newUrl = `/directory${params.toString() ? '?' + params.toString() : ''}`; | |
| 640 window.history.replaceState({ page: 'directory', dirPath: path }, '', newUrl); | |
| 641 setRoute(prev => ({ ...prev, dirPath: path })); | |
| 642 }, []); | |
| 643 | |
| 644 return ( | |
| 645 <div className="app-container"> | |
| 646 <Header | |
| 647 title="Zenbu Repository" | |
| 648 showThemeToggle={true} | |
| 649 isDark={isDark} | |
| 650 onToggleTheme={toggleTheme} | |
| 651 /> | |
| 652 | |
| 653 {/* Navigation Tabs */} | |
| 654 <div className="nav-tabs"> | |
| 655 <button | |
| 656 className={`nav-tab ${route.page === 'landing' ? 'active' : ''}`} | |
| 657 onClick={navigateToLanding} | |
| 658 > | |
| 659 Home | |
| 660 </button> | |
| 661 <button | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
662 className={`nav-tab ${route.page === 'graph' || route.page === 'changeset' ? 'active' : ''}`} |
| 193 | 663 onClick={() => navigateToGraph()} |
| 664 > | |
| 665 <GraphIcon /> | |
| 666 Graph | |
| 667 </button> | |
| 668 <button | |
| 669 className={`nav-tab ${route.page === 'directory' ? 'active' : ''}`} | |
| 670 onClick={() => navigateToDirectory()} | |
| 671 > | |
| 672 <FolderIcon /> | |
| 673 Files | |
| 674 </button> | |
| 675 </div> | |
| 676 | |
| 677 {/* Page Content */} | |
| 678 {route.page === 'landing' && ( | |
| 679 <LandingPage | |
| 680 onNavigateToGraph={() => navigateToGraph()} | |
| 681 onNavigateToDirectory={navigateToDirectory} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
682 onNavigateToChangeset={navigateToChangeset} |
| 193 | 683 /> |
| 684 )} | |
| 685 | |
| 686 {route.page === 'graph' && ( | |
| 687 <GraphPage | |
| 688 onBack={navigateToLanding} | |
| 689 initialCommit={route.graphCommit} | |
| 690 initialTip={route.graphTip} | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
691 onOpenChangeset={navigateToChangeset} |
| 193 | 692 /> |
| 693 )} | |
| 694 | |
| 695 {route.page === 'directory' && ( | |
| 696 <DirectoryPage | |
| 697 onBack={navigateToLanding} | |
| 698 initialPath={route.dirPath} | |
| 699 onPathChange={handleDirectoryPathChange} | |
| 700 /> | |
| 701 )} | |
| 702 | |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
703 {route.page === 'changeset' && route.changesetId && ( |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
704 <ChangesetPage |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
705 changesetId={route.changesetId} |
|
223
0e7b9464248d
[hg-web] Add browser route regression coverage
MrJuneJune <me@mrjunejune.com>
parents:
221
diff
changeset
|
706 onBack={navigateBackFromChangeset} |
|
221
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
707 onOpenChangeset={navigateToChangeset} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
708 /> |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
709 )} |
|
ce7f4400c2de
[hg-web] Harden forge and add changeset UI
MrJuneJune <me@mrjunejune.com>
parents:
193
diff
changeset
|
710 |
| 193 | 711 <Footer /> |
| 712 </div> | |
| 713 ); | |
| 714 } | |
| 715 | |
| 716 // App wrapper with ThemeProvider | |
| 717 function App() { | |
| 718 return ( | |
| 719 <ThemeProvider> | |
| 720 <AppContent /> | |
| 721 </ThemeProvider> | |
| 722 ); | |
| 723 } | |
| 724 | |
| 725 export { App }; |