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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
183 lines · 5.6 KB Raw History
<script lang="ts">
	import type { Label as LabelDef } from '#lib/lexicons/dev/knotless/repo.ts';
	import type * as defineLabel from '#lib/lexicons/dev/knotless/defineLabel.ts';
	import type * as removeLabel from '#lib/lexicons/dev/knotless/removeLabel.ts';
	import type { AtUriString } from '@atproto/lex';
	import { invalidateAll } from '$app/navigation';
	import { call } from '#lib/data.ts';
	import Label from '#lib/atoms/Label.svelte';
	import Dialog from '#lib/molecules/Dialog.svelte';
	import Empty from '#lib/molecules/Empty.svelte';
	/**
	 * The labels the repository `uri` names defines, for its pulls and
	 * issues to carry, listed: each made or changed in a dialog of its own,
	 * and taken away, as `knotless label define` and
	 * `remove` do.
	 */
	let { uri, labels }: { uri: AtUriString; labels: LabelDef[] } = $props();
	const DIALOG = 'label-dialog';
	/** The label being changed, by name, or '' for a new one. */
	let editing = $state('');
	let name = $state('');
	let color = $state('#8f8f8f');
	let description = $state('');
	let exclusive = $state(false);
	/** Why the dialog's write failed, and why a deletion in the list did. */
	let problem = $state('');
	let failed = $state('');
	// What is under way: 'save', or the label being deleted.
	let busy = $state('');
	/** Colours to pick with one click, as GitHub offers: its own label palette. */
	const presets = [
		'#b60205',
		'#d93f0b',
		'#fbca04',
		'#0e8a16',
		'#006b75',
		'#1d76db',
		'#0052cc',
		'#5319e7',
		'#e99695',
		'#c2e0c6',
		'#bfdadc',
		'#8f8f8f'
	];
	/** Fills the dialog with `l`, or empties it for a new label, before it opens. */
	function fill(l?: LabelDef) {
		editing = l?.name ?? '';
		name = l?.name ?? '';
		color = l?.color ?? presets[labels.length % presets.length];
		description = l?.description ?? '';
		exclusive = l?.exclusive ?? false;
		problem = '';
	}
	const dialog = () => document.getElementById(DIALOG) as HTMLElement | null;
	async function save(e: SubmitEvent) {
		e.preventDefault();
		busy = 'save';
		problem = '';
		try {
			await call<typeof defineLabel.main>('dev.knotless.defineLabel', {
				repo: uri,
				name: name.trim(),
				color,
				description,
				exclusive
			});
			await invalidateAll();
			dialog()?.hidePopover();
		} catch (err) {
			problem = (err as Error).message;
		}
		busy = '';
	}
	async function remove(l: LabelDef) {
		if (!confirm(`Delete ${l.name}? It comes off every pull and issue it is on.`)) return;
		busy = `delete ${l.name}`;
		failed = '';
		try {
			await call<typeof removeLabel.main>('dev.knotless.removeLabel', { repo: uri, name: l.name });
			await invalidateAll();
		} catch (err) {
			failed = (err as Error).message;
		}
		busy = '';
	}
</script>

<section class="setting labels-setting">
	<div class="setting-head">
		<h2>Labels</h2>
		<button class="btn primary" type="button" popovertarget={DIALOG} onclick={() => fill()}
			>New label</button
		>
	</div>
	{#if failed}<p class="problem" role="alert">{failed}</p>{/if}
	{#if labels.length}
		<ul class="box list rows">
			{#each labels as l (l.name)}
				<li>
					<span class="row-main"
						><Label label={l} />{#if l.exclusive}<span class="count">Exclusive</span
							>{/if}{#if l.description}<span class="mute">{l.description}</span>{/if}</span
					>
					<span class="row-end">
						<button
							class="btn"
							type="button"
							disabled={!!busy}
							popovertarget={DIALOG}
							onclick={() => fill(l)}>Edit</button
						>
						<button
							class="btn danger"
							type="button"
							disabled={!!busy}
							aria-busy={busy === `delete ${l.name}`}
							onclick={() => remove(l)}
							>{busy === `delete ${l.name}` ? 'Deleting…' : 'Delete'}</button
						>
					</span>
				</li>
			{/each}
		</ul>
	{:else}
		<div class="box">
			<Empty title="No labels yet">Pulls and issues carry only those defined here.</Empty>
		</div>
	{/if}
</section>

<Dialog id={DIALOG} class="label-dialog" title={editing ? 'Edit label' : 'New label'}>
	<div class="label-preview"><Label label={{ name: name.trim() || 'Label preview', color }} /></div>
	<form onsubmit={save}>
		<label for="label-name">Name</label>
		<input
			id="label-name"
			name="name"
			maxlength="50"
			placeholder="kind/bug"
			autocapitalize="none"
			spellcheck="false"
			required
			readonly={!!editing}
			bind:value={name}
		/>
		<label for="label-description">Description <span class="mute">(optional)</span></label>
		<input id="label-description" name="description" maxlength="200" bind:value={description} />
		<span class="label-like">Color</span>
		<div class="swatches" role="group" aria-label="Color">
			{#each presets as p (p)}<button
					type="button"
					class="swatch"
					style:--swatch={p}
					aria-label={p}
					aria-pressed={color === p}
					onclick={() => (color = p)}
				></button>{/each}
			<label class="swatch custom" style:--swatch={color} title="Any colour"
				><input type="color" aria-label="Any colour" bind:value={color} /></label
			>
			<code class="mute">{color}</code>
		</div>
		<label class="check exclusive"
			><input type="checkbox" bind:checked={exclusive} /><span>Exclusive</span><span class="mute"
				>Only one label with the same prefix can be on a pull or issue. Putting on priority/high
				takes priority/low off.</span
			></label
		>
		{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
		<div class="actions">
			<button class="btn" type="button" popovertarget={DIALOG} popovertargetaction="hide"
				>Cancel</button
			>
			<button
				class="btn primary"
				type="submit"
				disabled={!!busy || !name.trim()}
				aria-busy={busy === 'save'}
				>{busy === 'save' ? 'Saving…' : editing ? 'Save changes' : 'Create label'}</button
			>
		</div>
	</form>
</Dialog>