johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
<script lang="ts">
import type { ActorView, ReviewView } from '#lib/lexicons/dev/knotless/defs.ts';
import { plural } from '#lib/format.ts';
import Avatar from '#lib/atoms/Avatar.svelte';
import BotMark from '#lib/atoms/BotMark.svelte';
import Icon from '#lib/atoms/Icon.svelte';
import ActionMenu from '#lib/molecules/ActionMenu.svelte';
import Dialog from '#lib/molecules/Dialog.svelte';
/**
* A merge box's reviews, as GitHub's shows them: where they stand, then
* a row per reviewer, their latest verdict, linked to the comment that
* gave it. An approval of a head the pull has since moved past is of an
* earlier version; a request for changes stands until its reviewer
* reviews again. One the owner or a collaborator dismissed counts for
* neither. Those its author `requested` who have not reviewed it yet
* follow, awaited, those `required` by its base's rule as required. With `dismiss`, as the owner or a collaborator, each
* standing review's menu dismisses it, saying why.
*/
let {
pull,
reviews,
requested = [],
required = [],
dismiss
}: {
pull: string;
reviews: ReviewView[];
requested?: ActorView[];
required?: ActorView[];
dismiss?: (did: ActorView['did'], reason: string) => Promise<unknown>;
} = $props();
const awaited = $derived(requested.filter((a) => !reviews.some((r) => r.reviewer.did === a.did)));
const standing = $derived(reviews.filter((r) => !r.dismissed));
const asking = $derived(standing.filter((r) => r.verdict === 'changes'));
const approving = $derived(standing.filter((r) => r.verdict === 'approve' && !r.outdated));
const earlier = $derived(standing.filter((r) => r.verdict === 'approve' && r.outdated));
const set = $derived(reviews.length - standing.length);
const stance = $derived(
asking.length
? 'changes'
: approving.length
? 'approved'
: earlier.length
? 'earlier'
: awaited.length
? 'requested'
: 'dismissed'
);
const heading = $derived(
{
changes: 'Changes requested',
approved: 'Approved',
earlier: 'New changes since it was approved',
requested: 'Review requested',
dismissed: 'Reviews dismissed'
}[stance]
);
const row = $derived(
{
changes: 'failed',
approved: 'clean',
earlier: 'pending',
requested: 'pending',
dismissed: 'pending'
}[stance]
);
const icon = $derived(
(
{
changes: 'failed',
approved: 'passed',
earlier: 'running',
requested: 'running',
dismissed: 'running'
} as const
)[stance]
);
const counts = $derived(
[
asking.length ? `${plural(asking.length, 'review', 'reviews')} asking for changes` : '',
approving.length ? `${plural(approving.length, 'approval', 'approvals')}` : '',
earlier.length
? `${plural(earlier.length, 'approval', 'approvals')} of an earlier version`
: '',
awaited.length ? `${plural(awaited.length, 'review', 'reviews')} awaited` : '',
set ? `${plural(set, 'review', 'reviews')} dismissed` : ''
]
.filter(Boolean)
.join(', ')
);
const said = (r: ReviewView) =>
r.dismissed
? 'review dismissed'
: r.verdict === 'changes'
? 'asked for changes'
: r.outdated
? 'approved an earlier version'
: 'approved';
const anchor = (r: ReviewView) => `#comment-${r.uri.split('/').pop()}`;
const dialog = $derived(`dismiss-${pull.split('/').pop()}`);
/** The review being dismissed, once its menu was used. */
let chosen = $state<ReviewView | null>(null);
let reason = $state('');
let busy = $state(false);
let problem = $state('');
async function dismissing(e: SubmitEvent) {
e.preventDefault();
if (!chosen || !dismiss || !reason.trim()) return;
busy = true;
problem = '';
try {
await dismiss(chosen.reviewer.did, reason);
reason = '';
(document.getElementById(dialog) as HTMLElement | null)?.hidePopover();
} catch (err) {
problem = (err as Error).message;
}
busy = false;
}
</script>
{#if reviews.length || awaited.length}
<div class="row {row}">
<Icon name={icon} />
<div class="grow">
<b>{heading}</b>
<p class="mute">{counts}</p>
</div>
</div>
<div class="check-groups reviewers">
{#each reviews as r (r.uri)}
<div class="check-row">
<a class="check" href={anchor(r)}
><Avatar name={r.reviewer.handle} did={r.reviewer.did} src={r.reviewer.avatar} /><b
>{r.reviewer.handle}</b
>{#if r.reviewer.bot}<BotMark />{/if}<span class="said">{said(r)}</span><Icon
name="chevron"
/></a
>
{#if dismiss && !r.dismissed}<ActionMenu
label="Review options for {r.reviewer.handle}"
actions={[{ label: 'Dismiss review', opens: dialog, run: () => (chosen = r) }]}
/>{/if}
</div>
{/each}
{#each awaited as a (a.did)}
<span class="check"
><Avatar name={a.handle} did={a.did} src={a.avatar} /><b>{a.handle}</b>{#if a.bot}<BotMark
/>{/if}<span class="said"
>{required.some((q) => q.did === a.did) ? 'review required' : 'review requested'}</span
></span
>
{/each}
</div>
{/if}
{#if dismiss}
<Dialog id={dialog} title="Dismiss review" class="dismiss-dialog" held={busy}>
<form onsubmit={dismissing}>
{#if chosen}<p class="ask">
{`${chosen.reviewer.handle}'s review will stop counting until they review again.`}
</p>{/if}
<label for="{dialog}-reason">Why</label>
<textarea id="{dialog}-reason" name="reason" rows="3" required bind:value={reason}></textarea>
{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
<button class="btn primary" type="submit" disabled={busy || !reason.trim()} aria-busy={busy}
>{busy ? 'Dismissing…' : 'Dismiss review'}</button
>
</form>
</Dialog>
{/if}