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