johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
<script lang="ts">
import type { RepoViewBasic, RepoViewDetailed } from '#lib/lexicons/dev/knotless/defs.ts';
import type * as archiveRepo from '#lib/lexicons/dev/knotless/archiveRepo.ts';
import type * as deleteRepo from '#lib/lexicons/dev/knotless/deleteRepo.ts';
import type * as transferRepo from '#lib/lexicons/dev/knotless/transferRepo.ts';
import { goto, invalidateAll } from '$app/navigation';
import { call } from '#lib/data.ts';
import { urlPath } from '#lib/format.ts';
import type { AtIdentifierString } from '@atproto/lex';
import HandleInput from '#lib/molecules/HandleInput.svelte';
import Dialog from '#lib/molecules/Dialog.svelte';
/**
* What cannot be taken back, or not easily: offering `repo`, owned as
* `authority` names it, to another account, or withdrawing the offer
* it has, `offer`, as `knotless repo transfer` does; archiving it, making
* it read only, or unarchiving it, as `knotless repo archive` does; and
* deleting it, once
* its full name is typed, as `knotless repo delete` does. Transferring and
* deleting it are its owner's alone: unless it `owns` it, an admin of
* the organization that owns it archives it but is told who does those.
*/
let {
repo,
authority,
offer,
owns = true
}: {
repo: RepoViewDetailed;
authority: string;
offer?: RepoViewBasic;
owns?: boolean;
} = $props();
const full = $derived(`${authority}/${repo.name}`);
let to = $state('');
let as = $state('');
let typed = $state('');
let problem = $state('');
let archiveProblem = $state('');
let deleteProblem = $state('');
// Which is under way: 'offer', 'withdraw', 'archive' or 'delete'.
let busy = $state('');
async function transfer(e?: SubmitEvent) {
e?.preventDefault();
busy = e ? 'offer' : 'withdraw';
problem = '';
try {
const input = e
? { repo: repo.uri, to: to.trim() as AtIdentifierString, name: as.trim() || undefined }
: { repo: repo.uri };
await call<typeof transferRepo.main>('dev.knotless.transferRepo', input);
await invalidateAll();
to = as = '';
} catch (err) {
problem = (err as Error).message;
}
busy = '';
}
async function archive() {
busy = 'archive';
archiveProblem = '';
try {
await call<typeof archiveRepo.main>('dev.knotless.archiveRepo', {
repo: repo.uri,
unarchive: !!repo.archivedAt
});
await invalidateAll();
} catch (err) {
archiveProblem = (err as Error).message;
}
busy = '';
}
async function remove(e: SubmitEvent) {
e.preventDefault();
busy = 'delete';
deleteProblem = '';
try {
await call<typeof deleteRepo.main>('dev.knotless.deleteRepo', { repo: repo.uri });
await goto(`/${urlPath(authority)}`);
} catch (err) {
deleteProblem = (err as Error).message;
}
busy = '';
}
</script>
<section class="setting danger">
<h2>Danger zone</h2>
{#if owns}
<div class="danger-row">
<div>
<h3>Transfer ownership</h3>
{#if offer}
<p class="hint">
{`Offered to ${offer.handle}, as ${offer.handle}/${offer.name}: it moves once they accept it, from this repository's page.`}
</p>
{:else}
<p class="hint">
Offer it to another account. It moves, with its pulls, issues and forks, once they
accept; until then it stays here.
</p>
{/if}
</div>
{#if offer}<button
class="btn"
type="button"
disabled={!!busy}
aria-busy={busy === 'withdraw'}
onclick={() => transfer()}
>{busy === 'withdraw' ? 'Withdrawing…' : 'Withdraw offer'}</button
>{/if}
</div>
{#if !offer}
<form class="form transfer" onsubmit={transfer}>
<label for="transfer-to">Account</label>
<HandleInput id="transfer-to" name="to" autocomplete="off" bind:value={to} />
<label for="transfer-name">Name there <span class="mute">(optional)</span></label>
<input
id="transfer-name"
name="name"
placeholder={repo.name}
maxlength="100"
autocapitalize="none"
spellcheck="false"
bind:value={as}
/>
<div>
<button class="btn danger" type="submit" disabled={!!busy} aria-busy={busy === 'offer'}
>{busy === 'offer' ? 'Offering…' : 'Offer it'}</button
>
</div>
</form>
{/if}
{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
{/if}
<div class="danger-row">
<div>
{#if repo.archivedAt}
<h3>Unarchive this repository</h3>
<p class="hint">
It is read only now. Unarchiving lets pushes, pulls, issues and comments in again, and its
workflows run again.
</p>
{:else}
<h3>Archive this repository</h3>
<p class="hint">
Make it read only. Pushes, pulls, issues and comments are turned away, and its workflows
stop. You can unarchive it later.
</p>
{/if}
</div>
<button
class="btn danger"
type="button"
disabled={!!busy}
aria-busy={busy === 'archive'}
onclick={archive}
>{busy === 'archive'
? repo.archivedAt
? 'Unarchiving…'
: 'Archiving…'
: repo.archivedAt
? 'Unarchive'
: 'Archive'}</button
>
</div>
{#if archiveProblem}<p class="problem" role="alert">{archiveProblem}</p>{/if}
{#if owns}
<div class="danger-row">
<div>
<h3>Delete this repository</h3>
<p class="hint">
Its history, branches and tags go for good. Pulls, issues and comments others wrote stay
in their accounts.
</p>
</div>
<button class="btn danger" type="button" popovertarget="delete-repo">Delete</button>
</div>
{:else}
<p class="hint">{`Only ${authority} itself can transfer or delete this repository.`}</p>
{/if}
</section>
<Dialog id="delete-repo" title={`Delete ${full}`}>
<form onsubmit={remove}>
<label for="delete-typed">{`To confirm, type ${full}`}</label>
<input
id="delete-typed"
name="confirm"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
bind:value={typed}
/>
{#if deleteProblem}<p class="problem" role="alert">{deleteProblem}</p>{/if}
<button
class="btn danger solid"
type="submit"
disabled={!!busy || typed !== full}
aria-busy={busy === 'delete'}
>{busy === 'delete' ? 'Deleting…' : 'Delete this repository'}</button
>
</form>
</Dialog>