Lewis: May this revision serve well! lewis@tangled.org
+149
-7
Diff
round #1
+145
-1
appview/pages/templates/repo/fragments/diff.html
+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 = '✓';
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
+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
+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
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