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