Monorepo for Tangled tangled.org

appview/pages: PR mark-as-reviewed btn #1215

merged opened by oyster.cafe targeting master from lt/ap-mark-reviewed-btn
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mhpzlkussu22
+149 -7
Diff #1
+145 -1
appview/pages/templates/repo/fragments/diff.html
··· 14 14 {{ template "fragments/resizable" }} 15 15 {{ template "activeFileHighlight" }} 16 16 {{ template "fragments/line-quote-button" }} 17 + {{ template "reviewState" }} 17 18 </div> 18 19 {{ end }} 19 20 ··· 37 38 {{ $stat := $diff.Stats }} 38 39 {{ $count := len $diff.ChangedFiles }} 39 40 {{ template "repo/fragments/diffStatPill" $stat }} 40 - <span class="text-xs text-gray-600 dark:text-gray-400 hidden md:inline-flex">{{ $count }} changed file{{ if ne $count 1 }}s{{ end }}</span> 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> 41 42 42 43 {{ if $root }} 43 44 {{ if $root.IsInterdiff }} ··· 178 179 {{ end }} 179 180 </div> 180 181 </div> 182 + <label 183 + data-review-btn="file-{{ .Id }}" 184 + onclick="event.stopPropagation()" 185 + 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" 186 + title="Mark as reviewed" 187 + > 188 + <input 189 + type="checkbox" 190 + class="sr-only peer review-checkbox" 191 + data-file-id="file-{{ .Id }}" 192 + /> 193 + <span class="peer-checked:hidden">{{ i "circle" "size-4" }}</span> 194 + <span class="hidden peer-checked:inline text-green-600 dark:text-green-400">{{ i "circle-check" "size-4" }}</span> 195 + <span class="hidden md:inline">reviewed</span> 196 + </label> 181 197 </div> 182 198 </summary> 183 199 ··· 305 321 })(); 306 322 </script> 307 323 {{ end }} 324 + 325 + {{ define "reviewState" }} 326 + <script> 327 + (() => { 328 + const linkBase = document.getElementById('round-link-base'); 329 + if (!linkBase) return; 330 + 331 + const isInterdiff = !!document.getElementById('is-interdiff'); 332 + const basePath = linkBase.value.replace(/^\//, ''); 333 + const storageKey = 'reviewed:' + basePath + (isInterdiff ? '/interdiff' : ''); 334 + 335 + const REVIEWED_PREFIX = 'reviewed:'; 336 + const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; 337 + 338 + const load = () => { 339 + try { 340 + const entry = JSON.parse(localStorage.getItem(storageKey) || '{}'); 341 + return new Set(Array.isArray(entry) ? entry : (entry.files || [])); 342 + } 343 + catch { return new Set(); } 344 + }; 345 + 346 + const save = (reviewed) => { 347 + const liveIds = new Set(Array.from(allFiles()).map(d => d.id)); 348 + localStorage.setItem(storageKey, JSON.stringify({ 349 + files: Array.from(reviewed).filter(id => liveIds.has(id)), 350 + ts: Date.now(), 351 + })); 352 + }; 353 + 354 + const pruneStale = () => { 355 + const now = Date.now(); 356 + Object.keys(localStorage) 357 + .filter(k => k.startsWith(REVIEWED_PREFIX) && k !== storageKey) 358 + .forEach(k => { 359 + try { 360 + const entry = JSON.parse(localStorage.getItem(k)); 361 + if (!entry.ts || now - entry.ts > MAX_AGE_MS) localStorage.removeItem(k); 362 + } catch { localStorage.removeItem(k); } 363 + }); 364 + }; 365 + if (Math.random() < 0.1) pruneStale(); 366 + 367 + const allFiles = () => 368 + document.querySelectorAll('details[id^="file-"]'); 369 + 370 + const applyOne = (fileId, isReviewed) => { 371 + const detail = document.getElementById(fileId); 372 + if (!detail) return; 373 + 374 + const btn = detail.querySelector('[data-review-btn]'); 375 + const checkbox = btn?.querySelector('input[type="checkbox"]'); 376 + const path = CSS.escape(fileId.replace('file-', '')); 377 + const treeLink = document.querySelector(`.filetree-link[data-path="${path}"]`); 378 + 379 + detail.classList.toggle('opacity-60', isReviewed); 380 + 381 + if (checkbox) checkbox.checked = isReviewed; 382 + 383 + if (treeLink) { 384 + const existing = treeLink.parentElement.querySelector('.review-indicator'); 385 + if (isReviewed && !existing) { 386 + const indicator = document.createElement('span'); 387 + indicator.className = 'review-indicator text-green-600 dark:text-green-400 flex-shrink-0'; 388 + indicator.innerHTML = '&#10003;'; 389 + treeLink.parentElement.appendChild(indicator); 390 + } else if (!isReviewed && existing) { 391 + existing.remove(); 392 + } 393 + } 394 + }; 395 + 396 + const updateProgress = (reviewed) => { 397 + const el = document.getElementById('changed-files-label'); 398 + if (!el) return; 399 + const total = parseInt(el.dataset.total, 10); 400 + const files = allFiles(); 401 + const count = Array.from(files).filter(d => reviewed.has(d.id)).length; 402 + const suffix = total === 1 ? 'file' : 'files'; 403 + const allDone = count === total; 404 + el.classList.toggle('text-green-600', allDone); 405 + el.classList.toggle('dark:text-green-400', allDone); 406 + el.classList.toggle('text-gray-600', !allDone); 407 + el.classList.toggle('dark:text-gray-400', !allDone); 408 + el.textContent = count > 0 409 + ? `${count}/${total} ${suffix} reviewed` 410 + : `${total} changed ${suffix}`; 411 + }; 412 + 413 + const reviewed = load(); 414 + 415 + const toggleReview = (fileId) => { 416 + const detail = document.getElementById(fileId); 417 + if (!detail) return; 418 + const isNowReviewed = !reviewed.has(fileId); 419 + if (isNowReviewed) { 420 + reviewed.add(fileId); 421 + detail.open = false; 422 + } else { 423 + reviewed.delete(fileId); 424 + } 425 + save(reviewed); 426 + applyOne(fileId, isNowReviewed); 427 + updateProgress(reviewed); 428 + }; 429 + 430 + document.getElementById('diff-area').addEventListener('change', (e) => { 431 + const checkbox = e.target.closest('.review-checkbox'); 432 + if (!checkbox) return; 433 + const fileId = checkbox.dataset.fileId; 434 + if (fileId) toggleReview(fileId); 435 + }); 436 + 437 + document.querySelectorAll('.review-btn').forEach(btn => { 438 + btn.classList.remove('hidden'); 439 + btn.classList.add('flex'); 440 + }); 441 + 442 + allFiles().forEach(detail => { 443 + if (reviewed.has(detail.id)) { 444 + applyOne(detail.id, true); 445 + detail.open = false; 446 + } 447 + }); 448 + updateProgress(reviewed); 449 + })(); 450 + </script> 451 + {{ end }}
+3
appview/pages/templates/repo/pulls/pull.html
··· 100 100 101 101 {{ define "contentAfter" }} 102 102 <input type="hidden" id="round-link-base" value="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .ActiveRound }}" /> 103 + {{ if .IsInterdiff }} 104 + <input type="hidden" id="is-interdiff" value="1" /> 105 + {{ end }} 103 106 {{ template "repo/fragments/diff" (list .Diff .DiffOpts $) }} 104 107 {{ end }} 105 108
+1 -6
types/diff.go
··· 84 84 func (d NiceDiff) FileTree() *filetree.FileTreeNode { 85 85 fs := make([]string, len(d.Diff)) 86 86 for i, s := range d.Diff { 87 - n := s.Names() 88 - if n.New == "" { 89 - fs[i] = n.Old 90 - } else { 91 - fs[i] = n.New 92 - } 87 + fs[i] = s.Id() 93 88 } 94 89 return filetree.FileTree(fs) 95 90 }

History

2 rounds 3 comments
sign up or login to add to the discussion
1 commit
expand
appview/pages: PR mark-as-reviewed btn
3/3 success
expand
expand 2 comments

types/diff.go:87-87 guarantees the filetree path matches the DOM id

pull request successfully merged
1 commit
expand
appview/pages: PR mark-as-reviewed btn
3/3 success
expand
expand 1 comment

appview/pages/templates/repo/fragments/diff.html:182 this could probably be hacked into an actual literal checkbox input, but philosophically it's not part of a form it's just a button that does mark a thing as reviewed, and it just so happens to also toggle back in our case 馃し