~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
170 lines · 5.5 KB Raw History
<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}