johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
<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>