Lewis: May this revision serve well! lewis@tangled.org
+151
-1
Diff
round #0
+148
-1
appview/pages/templates/repo/fragments/diff.html
+148
-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
+
<button
183
+
type="button"
184
+
data-review-btn="file-{{ .Id }}"
185
+
onclick="event.preventDefault(); event.stopPropagation(); window.__reviewToggle && window.__reviewToggle('file-{{ .Id }}')"
186
+
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"
187
+
title="Mark as reviewed"
188
+
>
189
+
<span class="review-icon-unchecked">{{ i "circle" "size-4" }}</span>
190
+
<span class="review-icon-checked hidden">{{ i "circle-check" "size-4" }}</span>
191
+
<span class="hidden md:inline">reviewed</span>
192
+
</button>
181
193
</div>
182
194
</summary>
183
195
···
305
317
})();
306
318
</script>
307
319
{{ end }}
320
+
321
+
{{ define "reviewState" }}
322
+
<script>
323
+
(() => {
324
+
const linkBase = document.getElementById('round-link-base');
325
+
if (!linkBase) return;
326
+
327
+
const isInterdiff = !!document.getElementById('is-interdiff');
328
+
const basePath = linkBase.value.replace(/^\//, '');
329
+
const storageKey = 'reviewed:' + basePath + (isInterdiff ? '/interdiff' : '');
330
+
331
+
const REVIEWED_PREFIX = 'reviewed:';
332
+
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
333
+
334
+
const load = () => {
335
+
try {
336
+
const entry = JSON.parse(localStorage.getItem(storageKey) || '{}');
337
+
return new Set(Array.isArray(entry) ? entry : (entry.files || []));
338
+
}
339
+
catch { return new Set(); }
340
+
};
341
+
342
+
const save = (reviewed) => {
343
+
const liveIds = new Set(Array.from(allFiles()).map(d => d.id));
344
+
localStorage.setItem(storageKey, JSON.stringify({
345
+
files: Array.from(reviewed).filter(id => liveIds.has(id)),
346
+
ts: Date.now(),
347
+
}));
348
+
};
349
+
350
+
const pruneStale = () => {
351
+
const now = Date.now();
352
+
Object.keys(localStorage)
353
+
.filter(k => k.startsWith(REVIEWED_PREFIX) && k !== storageKey)
354
+
.forEach(k => {
355
+
try {
356
+
const entry = JSON.parse(localStorage.getItem(k));
357
+
if (!entry.ts || now - entry.ts > MAX_AGE_MS) localStorage.removeItem(k);
358
+
} catch { localStorage.removeItem(k); }
359
+
});
360
+
};
361
+
pruneStale();
362
+
363
+
const allFiles = () =>
364
+
document.querySelectorAll('details[id^="file-"]');
365
+
366
+
const applyOne = (fileId, isReviewed) => {
367
+
const detail = document.getElementById(fileId);
368
+
if (!detail) return;
369
+
370
+
const btn = detail.querySelector('[data-review-btn]');
371
+
const path = CSS.escape(fileId.replace('file-', ''));
372
+
const treeLink = document.querySelector(`.filetree-link[data-path="${path}"]`);
373
+
374
+
if (isReviewed) {
375
+
detail.classList.add('opacity-60');
376
+
if (btn) {
377
+
btn.classList.replace('text-gray-400', 'text-green-600');
378
+
btn.classList.replace('dark:text-gray-500', 'dark:text-green-400');
379
+
btn.querySelector('.review-icon-unchecked')?.classList.add('hidden');
380
+
btn.querySelector('.review-icon-checked')?.classList.remove('hidden');
381
+
}
382
+
if (treeLink) {
383
+
let indicator = treeLink.parentElement.querySelector('.review-indicator');
384
+
if (!indicator) {
385
+
indicator = document.createElement('span');
386
+
indicator.className = 'review-indicator text-green-600 dark:text-green-400 flex-shrink-0';
387
+
indicator.innerHTML = '✓';
388
+
treeLink.parentElement.appendChild(indicator);
389
+
}
390
+
}
391
+
} else {
392
+
detail.classList.remove('opacity-60');
393
+
if (btn) {
394
+
btn.classList.replace('text-green-600', 'text-gray-400');
395
+
btn.classList.replace('dark:text-green-400', 'dark:text-gray-500');
396
+
btn.querySelector('.review-icon-unchecked')?.classList.remove('hidden');
397
+
btn.querySelector('.review-icon-checked')?.classList.add('hidden');
398
+
}
399
+
if (treeLink) {
400
+
const indicator = treeLink.parentElement.querySelector('.review-indicator');
401
+
if (indicator) indicator.remove();
402
+
}
403
+
}
404
+
};
405
+
406
+
const updateProgress = (reviewed) => {
407
+
const el = document.getElementById('changed-files-label');
408
+
if (!el) return;
409
+
const total = parseInt(el.dataset.total, 10);
410
+
const files = allFiles();
411
+
const count = Array.from(files).filter(d => reviewed.has(d.id)).length;
412
+
const suffix = total === 1 ? 'file' : 'files';
413
+
const allDone = count === total;
414
+
el.classList.toggle('text-green-600', allDone);
415
+
el.classList.toggle('dark:text-green-400', allDone);
416
+
el.classList.toggle('text-gray-600', !allDone);
417
+
el.classList.toggle('dark:text-gray-400', !allDone);
418
+
el.textContent = count > 0
419
+
? `${count}/${total} ${suffix} reviewed`
420
+
: `${total} changed ${suffix}`;
421
+
};
422
+
423
+
const reviewed = load();
424
+
425
+
window.__reviewToggle = (fileId) => {
426
+
const detail = document.getElementById(fileId);
427
+
if (!detail) return;
428
+
const isNowReviewed = !reviewed.has(fileId);
429
+
if (isNowReviewed) {
430
+
reviewed.add(fileId);
431
+
detail.open = false;
432
+
} else {
433
+
reviewed.delete(fileId);
434
+
}
435
+
save(reviewed);
436
+
applyOne(fileId, isNowReviewed);
437
+
updateProgress(reviewed);
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 }}
+3
appview/pages/templates/repo/pulls/pull.html
+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
History
2 rounds
3 comments
oyster.cafe
submitted
#1
1 commit
expand
collapse
appview/pages: PR mark-as-reviewed btn
Lewis: May this revision serve well! <lewis@tangled.org>
3/3 success
expand
collapse
oyster.cafe
submitted
#0
1 commit
expand
collapse
appview/pages: PR mark-as-reviewed btn
Lewis: May this revision serve well! <lewis@tangled.org>
3/3 success
expand
collapse
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 馃し
types/diff.go:87-87guarantees the filetree path matches the DOM id