This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pages / templates / repo / fragments / diff.html
18 kB 454 lines
1{{ define "repo/fragments/diff" }} 2 <style> 3 #filesToggle:checked ~ div label[for="filesToggle"] .show-text { display: none; } 4 #filesToggle:checked ~ div label[for="filesToggle"] .hide-text { display: inline; } 5 #filesToggle:not(:checked) ~ div label[for="filesToggle"] .hide-text { display: none; } 6 #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; } 7 #filesToggle:not(:checked) ~ div div#files { width: 0; display: none; margin-right: 0; } 8 #filesToggle:not(:checked) ~ div div#resize-files { display: none; } 9 </style> 10 11 <div id="diff-area"> 12 {{ template "diffTopbar" . }} 13 {{ block "diffLayout" . }} {{ end }} 14 {{ template "fragments/resizable" }} 15 {{ template "activeFileHighlight" }} 16 {{ template "fragments/line-quote-button" }} 17 {{ template "reviewState" }} 18 </div> 19{{ end }} 20 21{{ define "diffTopbar" }} 22 {{ $diff := index . 0 }} 23 {{ $opts := index . 1 }} 24 {{ $root := "" }} 25 {{ if gt (len .) 2 }} 26 {{ $root = index . 2 }} 27 {{ end }} 28 29 {{ block "filesCheckbox" $ }} {{ end }} 30 {{ block "subsCheckbox" $ }} {{ end }} 31 32 <!-- top bar --> 33 <div class="sticky top-0 z-30 bg-slate-100 dark:bg-gray-900 flex items-center gap-2 col-span-full h-12 p-2 {{ if $root }}mt-4{{ end }}"> 34 <!-- left panel toggle --> 35 {{ template "filesToggle" . }} 36 37 <!-- stats --> 38 {{ $stat := $diff.Stats }} 39 {{ $count := len $diff.ChangedFiles }} 40 {{ template "repo/fragments/diffStatPill" $stat }} 41 <span id="changed-files-label" class="text-xs text-gray-600 dark:text-gray-400 hidden md:inline-flex" data-total="{{ $count }}">{{ $count }} changed file{{ if ne $count 1 }}s{{ end }}</span> 42 43 {{ if $root }} 44 {{ if $root.IsInterdiff }} 45 <!-- interdiff indicator --> 46 <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> 47 <span class="text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide">Interdiff</span> 48 <a 49 href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ sub $root.ActiveRound 1 }}" 50 class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" 51 > 52 #{{ sub $root.ActiveRound 1 }} 53 </a> 54 <span class="text-gray-400 text-xs">→</span> 55 <a 56 href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $root.ActiveRound }}" 57 class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" 58 > 59 #{{ $root.ActiveRound }} 60 </a> 61 </div> 62 {{ else if ne $root.ActiveRound nil }} 63 <!-- diff round indicator --> 64 <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> 65 <span class="text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide">Diff</span> 66 <span class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs border border-gray-300 dark:border-gray-600"> 67 <span class="hidden md:inline">round </span>#{{ $root.ActiveRound }} 68 </span> 69 </div> 70 {{ end }} 71 {{ end }} 72 73 <!-- spacer --> 74 <div class="flex-grow"></div> 75 76 <!-- collapse diffs --> 77 {{ template "collapseToggle" }} 78 79 <!-- diff options --> 80 {{ template "repo/fragments/diffOpts" $opts }} 81 82 <!-- right panel toggle --> 83 {{ block "subsToggle" $ }} {{ end }} 84 </div> 85 86{{ end }} 87 88{{ define "resize-grip" }} 89 {{ $id := index . 0 }} 90 {{ $target := index . 1 }} 91 {{ $direction := index . 2 }} 92 <div id="{{ $id }}" 93 data-resizer="vertical" 94 data-target="{{ $target }}" 95 data-direction="{{ $direction }}" 96 class="resizer-vertical hidden md:flex w-4 sticky top-12 max-h-screen flex-col items-center justify-center group"> 97 <div class="w-1 h-16 group-hover:h-24 group-[.resizing]:h-24 transition-all rounded-full bg-gray-400 dark:bg-gray-500 group-hover:bg-gray-500 group-hover:dark:bg-gray-400"></div> 98 </div> 99{{ end }} 100 101{{ define "diffLayout" }} 102 {{ $diff := index . 0 }} 103 {{ $opts := index . 1 }} 104 {{ $padBottom := gt (len .) 2 }} 105 106 <div class="flex col-span-full flex-grow"> 107 <!-- left panel --> 108 <div id="files" class="w-0 hidden md:block overflow-hidden sticky top-12 max-h-screen overflow-y-auto {{ if $padBottom }}pb-12{{ end }}"> 109 <section class="overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 110 {{ template "repo/fragments/fileTree" $diff.FileTree }} 111 </section> 112 </div> 113 114 {{ template "resize-grip" (list "resize-files" "files" "before") }} 115 116 <!-- main content --> 117 <div id="diff-files" class="flex-1 min-w-0 sticky top-12 {{ if $padBottom }}pb-12{{ end }}"> 118 {{ template "diffFiles" (list $diff $opts) }} 119 </div> 120 121 </div> 122{{ end }} 123 124{{ define "diffFiles" }} 125 {{ $diff := index . 0 }} 126 {{ $opts := index . 1 }} 127 {{ $files := $diff.ChangedFiles }} 128 {{ $isSplit := $opts.Split }} 129 <div class="flex flex-col gap-4"> 130 {{ if eq (len $files) 0 }} 131 <div class="text-center text-gray-500 dark:text-gray-400 py-8"> 132 <p>No differences found between the selected revisions.</p> 133 </div> 134 {{ else }} 135 {{ range $idx, $file := $files }} 136 {{ template "diffFile" (list $idx $file $isSplit) }} 137 {{ end }} 138 {{ end }} 139 </div> 140{{ end }} 141 142{{ define "diffFile" }} 143 {{ $idx := index . 0 }} 144 {{ $file := index . 1 }} 145 {{ $isSplit := index . 2 }} 146 {{ $collapsed := false }} 147 {{ if gt (len .) 3 }}{{ $collapsed = index . 3 }}{{ end }} 148 {{ $isGenerated := false }} 149 {{ $isDeleted := false }} 150 {{ with $file }} 151 {{ $n := .Names }} 152 {{ $isDeleted = and (eq $n.New "") (ne $n.Old "") }} 153 {{ if $n.New }} 154 {{ $isGenerated = isGenerated $n.New }} 155 {{ else if $n.Old }} 156 {{ $isGenerated = isGenerated $n.Old }} 157 {{ end }} 158 <details {{ if and (not $isGenerated) (not $isDeleted) (not $collapsed) }}open{{ end }} id="file-{{ .Id }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}"> 159 <summary class="list-none cursor-pointer {{ if not $collapsed }}sticky top-12{{ end }} group-open:border-b border-gray-200 dark:border-gray-700"> 160 <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 161 <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 162 <span class="group-open:hidden inline">{{ i "chevron-right" "w-4 h-4" }}</span> 163 <span class="hidden group-open:inline">{{ i "chevron-down" "w-4 h-4" }}</span> 164 {{ template "repo/fragments/diffStatPill" .Stats }} 165 166 <div class="flex gap-2 items-center overflow-x-auto"> 167 {{ if and $n.New $n.Old (ne $n.New $n.Old)}} 168 {{ $n.Old }} {{ i "arrow-right" "w-4 h-4" }} {{ $n.New }} 169 {{ else if $n.New }} 170 {{ $n.New }} 171 {{ else }} 172 {{ $n.Old }} 173 {{ end }} 174 {{ if $isDeleted }} 175 <span class="text-gray-400 dark:text-gray-500" title="Deleted files are collapsed by default"> 176 {{ i "circle-question-mark" "size-4" }} 177 </span> 178 {{ else if $isGenerated }} 179 <span class="text-gray-400 dark:text-gray-500" title="Generated files are collapsed by default"> 180 {{ i "circle-question-mark" "size-4" }} 181 </span> 182 {{ end }} 183 </div> 184 </div> 185 <label 186 data-review-btn="file-{{ .Id }}" 187 onclick="event.stopPropagation()" 188 class="review-btn hidden p-2 items-center gap-1 text-xs text-gray-400 dark:text-gray-500 hover:text-green-600 dark:hover:text-green-400 transition-colors cursor-pointer" 189 title="Mark as reviewed" 190 > 191 <input 192 type="checkbox" 193 class="sr-only peer review-checkbox" 194 data-file-id="file-{{ .Id }}" 195 /> 196 <span class="peer-checked:hidden">{{ i "circle" "size-4" }}</span> 197 <span class="hidden peer-checked:inline text-green-600 dark:text-green-400">{{ i "circle-check" "size-4" }}</span> 198 <span class="hidden md:inline">reviewed</span> 199 </label> 200 </div> 201 </summary> 202 203 <div class="transition-all duration-700 ease-in-out"> 204 {{ $reason := .CanRender }} 205 {{ if $reason }} 206 <p class="text-center text-gray-400 dark:text-gray-500 p-4">{{ $reason }}</p> 207 {{ else }} 208 {{ if $isSplit }} 209 {{- template "repo/fragments/splitDiff" .Split -}} 210 {{ else }} 211 {{- template "repo/fragments/unifiedDiff" . -}} 212 {{ end }} 213 {{- end -}} 214 </div> 215 </details> 216 {{ end }} 217{{ end }} 218 219{{ define "filesCheckbox" }} 220 <input type="checkbox" id="filesToggle" class="peer/files hidden" {{ if gt (len .) 2 }}checked{{ end }}/> 221{{ end }} 222 223{{ define "filesToggle" }} 224 <label title="Toggle filetree panel" for="filesToggle" class="hidden md:inline-flex items-center justify-center rounded cursor-pointer text-normal font-normal normalcase"> 225 <span class="show-text">{{ i "panel-left-open" "size-4" }}</span> 226 <span class="hide-text">{{ i "panel-left-close" "size-4" }}</span> 227 </label> 228{{ end }} 229 230{{ define "collapseToggle" }} 231 <label 232 title="Expand/Collapse diffs" 233 for="collapseToggle" 234 class="btn font-normal normal-case" 235 > 236 <input type="checkbox" id="collapseToggle" class="peer/collapse hidden" checked/> 237 <span class="peer-checked/collapse:hidden inline-flex items-center gap-2"> 238 {{ i "unfold-vertical" "w-4 h-4" }} 239 <span class="hidden md:inline">expand all</span> 240 </span> 241 <span class="peer-checked/collapse:inline-flex hidden flex items-center gap-2"> 242 {{ i "fold-vertical" "w-4 h-4" }} 243 <span class="hidden md:inline">collapse all</span> 244 </span> 245 </label> 246 <script> 247 (() => { 248 const checkbox = document.getElementById('collapseToggle'); 249 const diffArea = document.getElementById('diff-area'); 250 251 checkbox.addEventListener('change', () => { 252 document.querySelectorAll('details[id^="file-"]').forEach(detail => { 253 detail.open = checkbox.checked; 254 }); 255 }); 256 257 if (window.__collapseToggleHandler) { 258 diffArea.removeEventListener('toggle', window.__collapseToggleHandler, true); 259 } 260 261 const handler = (e) => { 262 if (!e.target.matches('details[id^="file-"]')) return; 263 const details = document.querySelectorAll('details[id^="file-"]'); 264 const allOpen = Array.from(details).every(d => d.open); 265 const allClosed = Array.from(details).every(d => !d.open); 266 267 if (allOpen) checkbox.checked = true; 268 else if (allClosed) checkbox.checked = false; 269 }; 270 271 window.__collapseToggleHandler = handler; 272 diffArea.addEventListener('toggle', handler, true); 273 })(); 274 </script> 275{{ end }} 276 277{{ define "activeFileHighlight" }} 278 <script> 279 (() => { 280 if (window.__activeFileScrollHandler) { 281 document.removeEventListener('scroll', window.__activeFileScrollHandler); 282 } 283 284 const filetreeLinks = document.querySelectorAll('.filetree-link'); 285 if (filetreeLinks.length === 0) return; 286 287 const linkMap = new Map(); 288 filetreeLinks.forEach(link => { 289 const path = link.getAttribute('data-path'); 290 if (path) linkMap.set('file-' + path, link); 291 }); 292 293 let currentActive = null; 294 const setActive = (link) => { 295 if (link && link !== currentActive) { 296 if (currentActive) currentActive.classList.remove('font-bold'); 297 link.classList.add('font-bold'); 298 currentActive = link; 299 } 300 }; 301 302 filetreeLinks.forEach(link => { 303 link.addEventListener('click', () => setActive(link)); 304 }); 305 306 const topbar = document.querySelector('.sticky.top-0.z-30'); 307 const headerHeight = topbar ? topbar.offsetHeight : 0; 308 309 const updateActiveFile = () => { 310 const diffFiles = document.querySelectorAll('details[id^="file-"]'); 311 Array.from(diffFiles).some(file => { 312 const rect = file.getBoundingClientRect(); 313 if (rect.top <= headerHeight && rect.bottom > headerHeight) { 314 setActive(linkMap.get(file.id)); 315 return true; 316 } 317 return false; 318 }); 319 }; 320 321 window.__activeFileScrollHandler = updateActiveFile; 322 document.addEventListener('scroll', updateActiveFile); 323 updateActiveFile(); 324 })(); 325 </script> 326{{ end }} 327 328{{ define "reviewState" }} 329 <script> 330 (() => { 331 const linkBase = document.getElementById('round-link-base'); 332 if (!linkBase) return; 333 334 const isInterdiff = !!document.getElementById('is-interdiff'); 335 const basePath = linkBase.value.replace(/^\//, ''); 336 const storageKey = 'reviewed:' + basePath + (isInterdiff ? '/interdiff' : ''); 337 338 const REVIEWED_PREFIX = 'reviewed:'; 339 const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; 340 341 const load = () => { 342 try { 343 const entry = JSON.parse(localStorage.getItem(storageKey) || '{}'); 344 return new Set(Array.isArray(entry) ? entry : (entry.files || [])); 345 } 346 catch { return new Set(); } 347 }; 348 349 const save = (reviewed) => { 350 const liveIds = new Set(Array.from(allFiles()).map(d => d.id)); 351 localStorage.setItem(storageKey, JSON.stringify({ 352 files: Array.from(reviewed).filter(id => liveIds.has(id)), 353 ts: Date.now(), 354 })); 355 }; 356 357 const pruneStale = () => { 358 const now = Date.now(); 359 Object.keys(localStorage) 360 .filter(k => k.startsWith(REVIEWED_PREFIX) && k !== storageKey) 361 .forEach(k => { 362 try { 363 const entry = JSON.parse(localStorage.getItem(k)); 364 if (!entry.ts || now - entry.ts > MAX_AGE_MS) localStorage.removeItem(k); 365 } catch { localStorage.removeItem(k); } 366 }); 367 }; 368 if (Math.random() < 0.1) pruneStale(); 369 370 const allFiles = () => 371 document.querySelectorAll('details[id^="file-"]'); 372 373 const applyOne = (fileId, isReviewed) => { 374 const detail = document.getElementById(fileId); 375 if (!detail) return; 376 377 const btn = detail.querySelector('[data-review-btn]'); 378 const checkbox = btn?.querySelector('input[type="checkbox"]'); 379 const path = CSS.escape(fileId.replace('file-', '')); 380 const treeLink = document.querySelector(`.filetree-link[data-path="${path}"]`); 381 382 detail.classList.toggle('opacity-60', isReviewed); 383 384 if (checkbox) checkbox.checked = isReviewed; 385 386 if (treeLink) { 387 const existing = treeLink.parentElement.querySelector('.review-indicator'); 388 if (isReviewed && !existing) { 389 const indicator = document.createElement('span'); 390 indicator.className = 'review-indicator text-green-600 dark:text-green-400 flex-shrink-0'; 391 indicator.innerHTML = '&#10003;'; 392 treeLink.parentElement.appendChild(indicator); 393 } else if (!isReviewed && existing) { 394 existing.remove(); 395 } 396 } 397 }; 398 399 const updateProgress = (reviewed) => { 400 const el = document.getElementById('changed-files-label'); 401 if (!el) return; 402 const total = parseInt(el.dataset.total, 10); 403 const files = allFiles(); 404 const count = Array.from(files).filter(d => reviewed.has(d.id)).length; 405 const suffix = total === 1 ? 'file' : 'files'; 406 const allDone = count === total; 407 el.classList.toggle('text-green-600', allDone); 408 el.classList.toggle('dark:text-green-400', allDone); 409 el.classList.toggle('text-gray-600', !allDone); 410 el.classList.toggle('dark:text-gray-400', !allDone); 411 el.textContent = count > 0 412 ? `${count}/${total} ${suffix} reviewed` 413 : `${total} changed ${suffix}`; 414 }; 415 416 const reviewed = load(); 417 418 const toggleReview = (fileId) => { 419 const detail = document.getElementById(fileId); 420 if (!detail) return; 421 const isNowReviewed = !reviewed.has(fileId); 422 if (isNowReviewed) { 423 reviewed.add(fileId); 424 detail.open = false; 425 } else { 426 reviewed.delete(fileId); 427 } 428 save(reviewed); 429 applyOne(fileId, isNowReviewed); 430 updateProgress(reviewed); 431 }; 432 433 document.getElementById('diff-area').addEventListener('change', (e) => { 434 const checkbox = e.target.closest('.review-checkbox'); 435 if (!checkbox) return; 436 const fileId = checkbox.dataset.fileId; 437 if (fileId) toggleReview(fileId); 438 }); 439 440 document.querySelectorAll('.review-btn').forEach(btn => { 441 btn.classList.remove('hidden'); 442 btn.classList.add('flex'); 443 }); 444 445 allFiles().forEach(detail => { 446 if (reviewed.has(detail.id)) { 447 applyOne(detail.id, true); 448 detail.open = false; 449 } 450 }); 451 updateProgress(reviewed); 452 })(); 453 </script> 454{{ end }}