chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
Labels are made and changed in a dialog, under the list
harness/e2e/flows.spec.mjs+11−5
| @@ -389,14 +389,20 @@ test('changing what a repository says about itself', async () => { | ||
| ||
test('defining, changing and deleting a label', async () => { | ||
await go(page, '/alice.test/tally/settings/labels') | ||
await page.getByLabel('New label').fill('kind/docs') | ||
await page.getByLabel(/^Description/).fill('Words, not code') | ||
await page.getByRole('button', { name: 'Create label' }).click() | ||
await page.getByRole('button', { name: 'New label' }).click() | ||
const dialog = page.getByRole('dialog', { name: 'New label' }) | ||
await dialog.getByLabel('Name').fill('kind/docs') | ||
await dialog.getByLabel(/^Description/).fill('Words, not code') | ||
await dialog.getByRole('button', { name: 'Create label' }).click() | ||
await expect(dialog).toBeHidden() | ||
const row = page.locator('li', { hasText: 'kind/docs' }) | ||
await expect(row).toContainText('Words, not code') | ||
await row.getByRole('button', { name: 'Edit' }).click() | ||
await page.getByLabel(/^Description/).fill('Words about the code') | ||
await page.getByRole('button', { name: 'Save label' }).click() | ||
const editing = page.getByRole('dialog', { name: 'Edit label' }) | ||
await expect(editing.getByLabel('Name')).toHaveValue('kind/docs') | ||
await editing.getByLabel(/^Description/).fill('Words about the code') | ||
await editing.getByRole('button', { name: 'Save changes' }).click() | ||
await expect(editing).toBeHidden() | ||
await expect(row).toContainText('Words about the code') | ||
await anew(page, (expect) => expect(page.locator('li', { hasText: 'kind/docs' })).toContainText('Words about the code')) | ||
page.once('dialog', (d) => d.accept()) | ||
harness/e2e/private.spec.mjs+5−2
| @@ -63,8 +63,11 @@ test('creating a private repository', async () => { | ||
| ||
test('defining a label', async () => { | ||
await go(page, '/alice.test/vault/settings/labels') | ||
await page.getByLabel('New label').fill('kind/secret') | ||
await page.getByRole('button', { name: 'Create label' }).click() | ||
await page.getByRole('button', { name: 'New label' }).click() | ||
const dialog = page.getByRole('dialog', { name: 'New label' }) | ||
await dialog.getByLabel('Name').fill('kind/secret') | ||
await dialog.getByRole('button', { name: 'Create label' }).click() | ||
await expect(dialog).toBeHidden() | ||
await expect(page.locator('li', { hasText: 'kind/secret' })).toBeVisible() | ||
expect(knotless('alice', 'labels', vault)).toContain('kind/secret') | ||
await anew(page, (expect) => expect(page.locator('li', { hasText: 'kind/secret' })).toBeVisible()) | ||
web/src/lib/molecules/Dialog.sveltenew+17−0
| @@ -0,0 +1,17 @@ | ||
<script lang="ts"> | ||
import type { Snippet } from 'svelte'; | ||
/** | ||
* A dialog over the dimmed page, opened by a button whose | ||
* `popovertarget` is its `id`, or by `showPopover`: its `title`, a | ||
* button to close it, then `children`. `class` names it for its own | ||
* styles; `ontoggle` hears it open and close. While `held`, it | ||
* cannot be closed by its button, as while a write it began is under way. | ||
*/ | ||
let { id, title, class: kind = '', held = false, ontoggle, children }: { id: string; title: string; class?: string; held?: boolean; ontoggle?: (e: ToggleEvent) => void; children: Snippet } = $props(); | ||
</script> | ||
| ||
<div class={['dialog', kind]} {id} popover="auto" role="dialog" aria-modal="true" aria-labelledby="{id}-title" {ontoggle}> | ||
<h2 id="{id}-title">{title}</h2> | ||
<button class="btn close" popovertarget={id} popovertargetaction="hide" aria-label="Close" disabled={held}>×</button> | ||
{@render children()} | ||
</div> | ||
web/src/lib/organisms/DangerZone.svelte+3−4
| @@ -8,6 +8,7 @@ | ||
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 | ||
| @@ -110,13 +111,11 @@ | ||
</div> | ||
</section> | ||
| ||
<div class="dialog" id="delete-repo" popover="auto" role="dialog" aria-modal="true" aria-labelledby="delete-title"> | ||
<h2 id="delete-title">{`Delete ${full}`}</h2> | ||
<button class="btn close" popovertarget="delete-repo" popovertargetaction="hide" aria-label="Close">×</button> | ||
<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> | ||
</div> | ||
</Dialog> | ||
web/src/lib/organisms/DeleteData.svelte+3−4
| @@ -5,6 +5,7 @@ | ||
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 delete-data` does: what there is, read when the | ||
| @@ -71,9 +72,7 @@ | ||
</div> | ||
</section> | ||
| ||
<div class="dialog delete-data" {id} popover="auto" role="dialog" aria-modal="true" aria-labelledby="{id}-title" ontoggle={(e) => (e as ToggleEvent).newState === 'open' && !held && read()}> | ||
<h2 id="{id}-title">Delete your knotless data</h2> | ||
<button class="btn close" popovertarget={id} popovertargetaction="hide" aria-label="Close" disabled={busy}>×</button> | ||
<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} | ||
| @@ -113,4 +112,4 @@ | ||
</form> | ||
{/if} | ||
{#if problem && !held}<p class="problem" role="alert">{problem}</p>{/if} | ||
</div> | ||
</Dialog> | ||
web/src/lib/organisms/LoginDialog.svelte+3−4
| @@ -1,6 +1,7 @@ | ||
<script lang="ts"> | ||
import { LOGIN } from '#lib/login.ts'; | ||
import HandleInput from '#lib/molecules/HandleInput.svelte'; | ||
import Dialog from '#lib/molecules/Dialog.svelte'; | ||
/** | ||
* Logging in with an Atmosphere account: the handle, then the account's | ||
* own server to approve it on, which sends the browser back to this page. | ||
| @@ -44,9 +45,7 @@ | ||
} | ||
</script> | ||
| ||
<div class="dialog" id={LOGIN} popover="auto" role="dialog" aria-modal="true" aria-labelledby="login-title"> | ||
<h2 id="login-title">Log in with your Atmosphere account</h2> | ||
<button class="btn close" popovertarget={LOGIN} popovertargetaction="hide" aria-label="Close">×</button> | ||
<Dialog id={LOGIN} title="Log in with your Atmosphere account"> | ||
<div class="apps" aria-hidden="true"> | ||
<div class="apps-run"> | ||
{#each [...apps, ...apps] as app, i (i)}<img src={icon(app)} alt="" width="40" height="40" loading="lazy" />{/each} | ||
| @@ -64,4 +63,4 @@ | ||
</div> | ||
</form> | ||
<p class="create">Don't have an account? <button type="button" onclick={() => login(true)} disabled={!!busy}>{busy === 'create' ? 'Opening…' : 'Create one'}</button></p> | ||
</div> | ||
</Dialog> | ||
web/src/lib/organisms/MergeBox.svelte+3−4
| @@ -16,6 +16,7 @@ | ||
import Command from '#lib/molecules/Command.svelte'; | ||
import MergeChecks from '#lib/organisms/MergeChecks.svelte'; | ||
import Reviewers from '#lib/organisms/Reviewers.svelte'; | ||
import Dialog from '#lib/molecules/Dialog.svelte'; | ||
/** | ||
* The open pull `pull` on `repo`, into `base` from `head`, as GitHub's | ||
* merge box shows one: where its `reviews` stand, its `checks`, then whether it merges with its | ||
| @@ -187,9 +188,7 @@ | ||
{/if} | ||
</div> | ||
{#if s.may.base || s.may.head} | ||
<div class="dialog" id={retarget} popover="auto" role="dialog" aria-modal="true" aria-labelledby="{retarget}-title"> | ||
<h2 id="{retarget}-title">Retarget this pull</h2> | ||
<button class="btn close" popovertarget={retarget} popovertargetaction="hide" aria-label="Close">×</button> | ||
<Dialog id={retarget} title="Retarget this pull"> | ||
<form onsubmit={point}> | ||
{#if s.may.base} | ||
<label for="{retarget}-base">Base branch</label> | ||
| @@ -203,7 +202,7 @@ | ||
{/if} | ||
<button class="btn primary" type="submit" disabled={!!busy} aria-busy={busy === 'retarget'}>{busy === 'retarget' ? 'Retargeting…' : 'Retarget'}</button> | ||
</form> | ||
</div> | ||
</Dialog> | ||
{/if} | ||
{:else if logged && !merged && !problem} | ||
<div class="row"><Icon name="running" /><span class="mute">Checking whether it can merge…</span></div> | ||
web/src/lib/organisms/OrgDangerZone.svelte+3−4
| @@ -3,6 +3,7 @@ | ||
import { goto, invalidateAll } from '$app/navigation'; | ||
import { call } from '#lib/data.ts'; | ||
import { urlPath } from '#lib/format.ts'; | ||
import Dialog from '#lib/molecules/Dialog.svelte'; | ||
/** | ||
* Ending the organization the account `handle` is, once its handle is | ||
* typed, as `knotless org delete` does: every member taken out, every | ||
| @@ -46,13 +47,11 @@ | ||
</section> | ||
</div> | ||
| ||
<div class="dialog" id="delete-org" popover="auto" role="dialog" aria-modal="true" aria-labelledby="delete-org-title"> | ||
<h2 id="delete-org-title">Delete organization</h2> | ||
<button class="btn close" popovertarget="delete-org" popovertargetaction="hide" aria-label="Close">×</button> | ||
<Dialog id="delete-org" title="Delete organization"> | ||
<form onsubmit={end}> | ||
<label for="delete-org-typed">{`To confirm, type ${handle}`}</label> | ||
<input id="delete-org-typed" name="confirm" autocapitalize="none" autocomplete="off" spellcheck="false" bind:value={typed} /> | ||
{#if problem}<p class="problem" role="alert">{problem}</p>{/if} | ||
<button class="btn danger solid" type="submit" disabled={busy || typed !== handle} aria-busy={busy}>{busy ? 'Deleting…' : 'Delete organization'}</button> | ||
</form> | ||
</div> | ||
</Dialog> | ||
web/src/lib/organisms/OrgMembers.svelte+3−4
| @@ -13,6 +13,7 @@ | ||
import Delegate from '#lib/molecules/Delegate.svelte'; | ||
import MemberList from '#lib/organisms/MemberList.svelte'; | ||
import HandleInput from '#lib/molecules/HandleInput.svelte'; | ||
import Dialog from '#lib/molecules/Dialog.svelte'; | ||
/** | ||
* The Members tab of the account `did`, called `handle`, as `me` sees | ||
* it, from what `org` answers: its members and invitations, and what | ||
| @@ -194,14 +195,12 @@ | ||
<p class="feed-note">To end the organization, go to <a href="/settings/organization">its settings</a>.</p> | ||
{/if} | ||
| ||
<div class="dialog" id="org-confirm" popover="auto" role="dialog" aria-modal="true" aria-labelledby="org-confirm-title"> | ||
<Dialog id="org-confirm" title={asking?.title ?? ''}> | ||
{#if asking} | ||
<h2 id="org-confirm-title">{asking.title}</h2> | ||
<button class="btn close" popovertarget="org-confirm" popovertargetaction="hide" aria-label="Close">×</button> | ||
<p class="ask">{asking.body}</p> | ||
<div class="actions"> | ||
<button class="btn" type="button" popovertarget="org-confirm" popovertargetaction="hide">Cancel</button> | ||
<button class="btn danger solid" type="button" disabled={!!busy} aria-busy={busy === 'confirm'} onclick={confirm}>{busy === 'confirm' ? asking.busy : asking.ok}</button> | ||
</div> | ||
{/if} | ||
</div> | ||
</Dialog> | ||
web/src/lib/organisms/ReportDialog.svelte+3−4
| @@ -2,6 +2,7 @@ | ||
import type * as createReport from '#lib/lexicons/dev/knotless/createReport.ts'; | ||
import { call } from '#lib/data.ts'; | ||
import { REPORT, reporting } from '#lib/report.svelte.ts'; | ||
import Dialog from '#lib/molecules/Dialog.svelte'; | ||
/** | ||
* Reporting what `reporting` names to the moderation service, as whoever | ||
* is logged in: why, from atproto's reasons, and more in words. | ||
| @@ -41,9 +42,7 @@ | ||
} | ||
</script> | ||
| ||
<div class="dialog" id={REPORT} popover="auto" role="dialog" aria-modal="true" aria-labelledby="report-title" ontoggle={toggled}> | ||
<h2 id="report-title">{`Report ${reporting.what || 'this'}`}</h2> | ||
<button class="btn close" popovertarget={REPORT} popovertargetaction="hide" aria-label="Close">×</button> | ||
<Dialog id={REPORT} title={`Report ${reporting.what || 'this'}`} ontoggle={toggled}> | ||
{#if sent} | ||
<p class="sent">Sent to the moderators. Thank you.</p> | ||
{:else} | ||
| @@ -58,4 +57,4 @@ | ||
<button class="btn primary" type="submit" disabled={busy || !reporting.subject} aria-busy={busy}>{busy ? 'Sending…' : 'Send report'}</button> | ||
</form> | ||
{/if} | ||
</div> | ||
</Dialog> | ||