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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
159 lines · 5.4 KB Raw History
<script lang="ts">
	import type * as getAccountData from '#lib/lexicons/dev/knotless/getAccountData.ts';
	import type * as deleteAccountData from '#lib/lexicons/dev/knotless/deleteAccountData.ts';
	import type { DidString } from '@atproto/lex';
	import { goto, invalidateAll } from '$app/navigation';
	import { ask, call, type Answer } from '#lib/data.ts';
	import { urlPath } from '#lib/format.ts';
	import Dialog from '#lib/molecules/Dialog.svelte';
	/**
	 * Deleting every knotless record from the account `did`, logged in as
	 * `handle`, as `knotless account delete-data` does: what there is, read when the
	 * dialog opens, its repositories others push to or read named with a
	 * way to transfer them first; then, once the handle is typed, the
	 * deleting, a part a call, and the front page, logged out.
	 */
	let { did, handle }: { did: string; handle: string } = $props();
	const id = 'delete-data';
	/** The records and spaces one call deletes: a part, for the progress shown. */
	const PART = 100;
	let held = $state<Answer<typeof getAccountData.main> | null>(null);
	let typed = $state('');
	let problem = $state('');
	let reading = $state(false);
	let busy = $state(false);
	let deleted = $state(0);
	/** How many records, and other accounts, as a person reads it. */
	const n = (count: number) => count.toLocaleString('en-US');
	const records = (count: number) => (count === 1 ? '1 record' : `${n(count)} records`);
	const others = (count: number) =>
		count === 1 ? '1 other account' : `${n(count)} other accounts`;
	const total = $derived(
		(held?.collections.reduce((n, c) => n + c.count, 0) ?? 0) + (held?.spaces ?? 0)
	);
	const summary = $derived(
		held?.spaces
			? `This deletes ${records(total - held.spaces)} and ${held.spaces === 1 ? '1 private repository space' : `${held.spaces} private repository spaces`} from your account, for good.`
			: `This deletes ${records(total)} from your account, for good.`
	);
	const shared = $derived(
		held?.repos.filter((r) => r.collaborators > 0 || (r.members ?? 0) > 0) ?? []
	);

	async function read() {
		reading = true;
		problem = '';
		try {
			held = await ask<typeof getAccountData.main>('dev.knotless.getAccountData', {});
		} catch (e) {
			problem = (e as Error).message;
		}
		reading = false;
	}
	async function remove(e: SubmitEvent) {
		e.preventDefault();
		busy = true;
		problem = '';
		try {
			for (;;) {
				const step = await call<typeof deleteAccountData.main>('dev.knotless.deleteAccountData', {
					did: did as DidString,
					limit: PART
				});
				deleted += step.deleted;
				if (step.done) break;
			}
			await goto('/');
			await invalidateAll();
		} catch (err) {
			problem = `${(err as Error).message} What is deleted stays deleted: try again to carry on.`;
			busy = false;
		}
	}
</script>

<section class="setting danger account-danger">
	<h2>Delete your knotless data</h2>
	<div class="danger-row">
		<div>
			<p class="hint">
				Every knotless record in your account goes for good: your repositories, private ones too,
				and your pulls, issues, comments, stars and follows. You are then logged out. Your atproto
				account, and what other apps keep in it, stay.
			</p>
		</div>
		<button class="btn danger" type="button" popovertarget={id}>Delete data</button>
	</div>
</section>

<Dialog
	{id}
	class="delete-data"
	title="Delete your knotless data"
	held={busy}
	ontoggle={(e) => e.newState === 'open' && !held && read()}
>
	{#if reading}
		<p class="hint" data-loading>Reading what your account holds…</p>
	{:else if held && total === 0}
		<p class="hint">Your account holds no knotless data.</p>
	{:else if held}
		<div class="data-body">
			<p class="hint">{summary}</p>
			{#if held.repos.length}
				<h3>Repositories</h3>
				{#if shared.length}<p class="hint">
						Others use a repository shared with them: transfer it to one of them first to keep it.
					</p>{/if}
				<ul class="data-repos">
					{#each held.repos as r (r.name)}
						{@const sharing = Math.max(r.collaborators, r.members ?? 0)}
						<li>
							<span class="clip">{r.name}</span>
							{#if r.private}<span class="mute">Private</span>{/if}
							{#if sharing}<span class="shared"
									><span class="mute">{`Shared with ${others(sharing)}`}</span><a
										href="/{urlPath(handle)}/{urlPath(r.name)}/settings">Transfer</a
									></span
								>{/if}
						</li>
					{/each}
				</ul>
			{/if}
			{#if held.collections.length}
				<h3>Records</h3>
				<ul class="data-counts">
					{#each held.collections as c (c.collection)}
						<li>
							<code>{c.collection.replace(/^dev\.knotless\./, '')}</code><span class="mute"
								>{n(c.count)}</span
							>
						</li>
					{/each}
				</ul>
			{/if}
		</div>
		<form onsubmit={remove}>
			<label for="{id}-typed">{`To confirm, type ${handle}`}</label>
			<input
				id="{id}-typed"
				name="confirm"
				autocapitalize="none"
				autocomplete="off"
				spellcheck="false"
				disabled={busy}
				bind:value={typed}
			/>
			{#if busy}<progress max={total} value={deleted}>{`${deleted} of ${total}`}</progress>
				<p class="hint" role="status">{`Deleted ${n(deleted)} of ${n(total)}…`}</p>{/if}
			{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
			<button
				class="btn danger solid"
				type="submit"
				disabled={busy || typed.trim() !== handle}
				aria-busy={busy}>{busy ? 'Deleting…' : 'Delete all my knotless data'}</button
			>
		</form>
	{/if}
	{#if problem && !held}<p class="problem" role="alert">{problem}</p>{/if}
</Dialog>