chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A pull opens as a draft, and its author makes it a draft again, from its page
docs/design/collaboration.md+9−6
| @@ -75,7 +75,7 @@ so a comment on a pull opened a moment ago never waits for an index. | ||
| ||
### Reviews | ||
| ||
Reviews stand as GitHub's do. Each reviewer's latest comment with a verdict | ||
Each reviewer's latest comment with a verdict | ||
is where they stand; a comment without one changes nothing, and a later | ||
verdict replaces the earlier. The pull's author reviews nothing of their | ||
own: the client refuses, and readers pass over any such verdict, as they do | ||
| @@ -89,7 +89,7 @@ pull's merge box, and `knotless show` lists them as `review:` lines. | ||
### Review agents | ||
| ||
A review needs no person behind it. `knotless-agent review` is an account | ||
that reviews the pulls asking it, as GitHub's review bots do: a command, | ||
that reviews the pulls asking it: a command, | ||
by default Claude Code (`knotless-claude-review`), reads each pull and | ||
answers a verdict, a body and comments on lines, and the agent writes them | ||
as its own review, with the head it read. It reviews a pull again when it | ||
| @@ -128,7 +128,7 @@ removes, otherwise the side it brings), the last `line` and, for more than | ||
one, the `startLine`, counted in the file as it is on that side. Its | ||
`commit` is the pull's head as it was then, which the lines are of. | ||
| ||
Comments on lines go one of two ways, as on GitHub. Sent with a body or a | ||
Comments on lines go one of two ways. Sent with a body or a | ||
verdict, they are that review's: the review and each comment, naming the | ||
review in `review`, are written in one `applyWrites`. With neither, each | ||
stands alone. On the pages, the `+` on a line of the diff (shift-click a | ||
| @@ -179,7 +179,10 @@ they have not reviewed it; once it is merged or closed, no longer. | ||
| ||
Both are fields of the pull record, so only its author sets them: when | ||
opening it (`knotless pull --draft --reviewer <handle>`) and with `knotless | ||
edit` (`--draft`, `--ready`, `--reviewer`, `--no-reviewer`). A maintainer | ||
edit` (`--draft`, `--ready`, `--reviewer`, `--no-reviewer`). On the pages, | ||
the author opens one as a draft with Open as draft, marks it | ||
ready with the merge box's Ready for review, and makes it a draft again | ||
with Convert to draft in the merge box's More menu. A maintainer | ||
cannot ask someone to review another person's pull: that would need a | ||
record of the maintainer's own, which knotless does not have yet. | ||
| ||
| @@ -437,7 +440,7 @@ One parser (`crates/knotless/src/query.rs`) reads the query everywhere: | ||
items it matches; `knotless list -q`; and the MCP's `list_items`. The web | ||
pages keep it in the address (`?q=`), so a filtered list can be linked. | ||
Their Author and Labels menus, and a click on a label in a row, change the | ||
query a word at a time, as GitHub's do, so the menus teach the words. | ||
query a word at a time, so the menus teach the words. | ||
| ||
## Finding everything said about a repository | ||
| ||
| @@ -542,7 +545,7 @@ Details: | ||
## Reverting a pull | ||
| ||
`knotless revert`, the MCP `revert` tool and a merged pull's Revert button | ||
undo a pull as GitHub's button does: they never merge, they open a pull | ||
undo a pull without merging: they never merge, they open a pull | ||
that undoes it, to check and merge like any other. The owner and | ||
collaborators may revert, as they may merge. | ||
| ||
docs/design/merge-queue.md+1−2
| @@ -127,8 +127,7 @@ A pull's own checks are its pre-flight: they say it builds and passes at | ||
its head merged with the branch, but not what the batch's checks hold it | ||
to on top of the others. A merge past the queue lands what only the | ||
pull's own checks passed. A branch whose rule says `queue` (`knotless | ||
protect <url> <branch> --queue`) takes pulls only through the queue, | ||
as GitHub's "Require merge queue" does: | ||
protect <url> <branch> --queue`) takes pulls only through the queue: | ||
| ||
- A plain merge (`knotless merge`, MCP `merge`) queues the pull instead | ||
and says so (`main takes pulls through the merge queue: queued, 1 of | ||
docs/design/serve.md+19−22
| @@ -733,8 +733,7 @@ comment, naming the asker's own reaction where there is an asker, and | ||
never to what its owner hid, and a reaction the pages write is indexed at | ||
once. The pages, `knotless react` and MCP's `react` share the code that | ||
writes it: reacting the same way twice keeps the first, and taking it | ||
back deletes every record of it. As on GitHub, a reaction notifies no | ||
one. | ||
back deletes every record of it. A reaction notifies no one. | ||
| ||
On a private repository, a reaction is written in the reacting member's | ||
repo in its space, as a comment is, so it names nothing to anyone else. | ||
| @@ -745,7 +744,7 @@ each look reads it, with the pulls and issues. `knotless show` and MCP's | ||
| ||
### Landing and checks | ||
| ||
A pull's page ends in a merge box, as GitHub's does. An open pull's | ||
A pull's page ends in a merge box. An open pull's | ||
checks show there, not in the timeline: how many are in each state, then | ||
a row for each, those that failed or are running open and the rest | ||
folded, each linked to its run's page, which has its log and the | ||
| @@ -808,7 +807,7 @@ refreshed, so the page it goes on to, whose empty state says how to push, | ||
and the account's profile show it. | ||
| ||
A repository's owner sees a Settings tab, at `settings`, with a side nav | ||
of its sections, as GitHub's. General sets its description, links, topics | ||
of its sections. General sets its description, links, topics | ||
and default branch (`knotless describe`), renames it, shows what it takes up and | ||
compacts it, and has a Danger zone to offer it to another account or | ||
withdraw the offer, and to delete it once its full name is typed. | ||
| @@ -852,13 +851,12 @@ server, read from the owner's PDS once and checked against its release. | ||
| ||
Logged in, someone opens an issue from the Issues list's New issue, its | ||
body started from the repository's issue template, and a pull from the | ||
Pulls list's New pull, or Compare & pull on a branch's files: as GitHub's | ||
compare page does, it chooses the branch to merge into, then the branch to | ||
Pulls list's New pull, or Compare & pull on a branch's files: it chooses the branch to merge into, then the branch to | ||
bring in, from the repository or a fork of it, shows the commits and diff | ||
it would bring (`getCompare`), with a button that unfolds the form to open | ||
it, its body started from the pull template at that base (`getTemplate`). | ||
Compare & pull links with `expand=1`, which shows the form at once, as | ||
GitHub's banner does. Beside the form, as GitHub's is, a sidebar | ||
Compare & pull links with `expand=1`, which shows the form at once. | ||
Beside the form, a sidebar | ||
chooses the labels to put on it, those the repository's viewer state says | ||
the account may (`getRepo`'s `viewer.labels`), and for a pull the | ||
reviewers to ask, from the owner and collaborators. Opening (`createIssue`, | ||
| @@ -867,8 +865,8 @@ pull` and `knotless label`, so a body that follows none of the template is | ||
refused there too, and a label the account may not put on is refused | ||
before anything is written. | ||
| ||
A pull's or an issue's page is written on as the account logged in, as | ||
GitHub's is: a comment at the end of the thread, and on someone else's | ||
A pull's or an issue's page is written on as the account logged in: | ||
a comment at the end of the thread, and on someone else's | ||
open pull a review, Comment, Approve or Request changes. Each author edits | ||
and deletes their own comments, and edits the title and body of what they | ||
opened. Each write is a procedure on the server (`createComment`, | ||
| @@ -888,8 +886,8 @@ Reporting is in each comment's "…" menu, for anyone. Logged out, the | ||
controls there open the login dialog. A hidden comment leaves the page | ||
at once, with an Undo; a hidden pull or issue leaves with its page. | ||
Each box written in Markdown (a new issue's or pull's body, a comment, a | ||
review, an edit of a body or a comment) has Write and Preview tabs, as | ||
GitHub's has. Preview asks `renderMarkdown`, which draws the text with the | ||
review, an edit of a body or a comment) has Write and Preview tabs. | ||
Preview asks `renderMarkdown`, which draws the text with the | ||
renderer the thread's comments are drawn with, so it shows what will be | ||
shown once posted. It needs no login, reads nothing and fetches nothing, | ||
and takes no more than a body may hold. It is a reading aid for the box | ||
| @@ -909,10 +907,9 @@ path and a line, which `dev.knotless.comment` cannot yet. | ||
An account hears of what others did that concerns it: a star of one of | ||
its repositories, a follow of it, a pull or an issue opened on a | ||
repository it looks after, and a comment on one of its pulls or issues, or on one it | ||
commented on, from after it did, as GitHub subscribes a thread's | ||
participants; a pull, an issue or a comment that mentions it; and an | ||
commented on, from after it did; a pull, an issue or a comment that mentions it; and an | ||
invitation to an organization. It hears too of its own pulls' checks, | ||
from the runners its repository names, as GitHub tells the one who pushed: | ||
from the runners its repository names: | ||
`checks-failed` when checks fail at a pull's latest run, and `ejected` | ||
when the merge queue takes the pull out ([merge-queue.md](merge-queue.md)). | ||
A | ||
| @@ -925,7 +922,7 @@ mentions the account, not a `mention` one. | ||
| ||
The repositories an account looks after are its own, those whose records | ||
name it a collaborator, and every repository of an organization it has | ||
joined, in public or hidden, as GitHub tells a repository's maintainers. | ||
joined, in public or hidden. | ||
The index keeps who each repository's record names in a table, so that | ||
asking does not read every record; the organizations an account joined | ||
it reads from both indexes, as a hidden membership is only in the | ||
| @@ -1009,7 +1006,7 @@ the pull has merged or closed: there is nothing left to do about it. An ejection | ||
`dev.knotless.queue` record turning the pull away, with the checks that | ||
failed when it was for them. Neither is any account's doing, so hearing | ||
only of accounts one follows leaves them in. The repository's owner | ||
hears of neither for others' pulls, as GitHub tells only who pushed. | ||
hears of neither for others' pulls: only the author does. | ||
| ||
A browser the pages' settings turned it on in shows each new | ||
notification as the system's own while a tab is open but out of sight: | ||
| @@ -1053,11 +1050,11 @@ few accounts. | ||
| ||
### Settings | ||
| ||
`/settings` is the logged-in account's, laid out as GitHub's are, with its | ||
`/settings` is the logged-in account's, with its | ||
sections down the side: its public profile, the addresses its commits show | ||
as its by, its appearance, its notifications, its delegated writes, and | ||
the accounts it blocks. A section of more than one page is a group, as | ||
GitHub's are: Appearance's name opens it, to Themes and Make a theme, and | ||
the accounts it blocks. A section of more than one page is a group: | ||
Appearance's name opens it, to Themes and Make a theme, and | ||
it stays open while one of them is the page; on a phone, where the | ||
sections are a row of tabs, it is one tab. The profile is | ||
its `dev.knotless.profile`, field by field over the Bluesky profile, with | ||
| @@ -1084,7 +1081,7 @@ here, and the pages refuse to queue it. | ||
Appearance chooses the colours the pages are drawn in: the system's light | ||
or dark, as with no choice, light or dark always, or a theme after an | ||
editor's (Catppuccin, Dracula) or an app's of the Atmosphere (pckt, | ||
plyr.fm). Or two, Sync with system, as GitHub calls it: a day theme | ||
plyr.fm). Or two, Sync with system: a day theme | ||
while the system is light and a night theme while it is dark, each | ||
chosen in its own tab over the one list of themes, which marks each in | ||
use with a sun or a moon. The browser | ||
| @@ -1111,7 +1108,7 @@ Anyone can make a theme of their own colours, and publish it for anyone to | ||
choose. A `dev.knotless.theme` record holds one: a name, and a palette of | ||
colours named for what they colour (`background`, `text`, `textMuted`, | ||
`primary`, `accent`; the states' `success`, `danger`, `attention`, `done` | ||
and `neutral`, as GitHub's Primer names them; and code's), each `#rrggbb`. | ||
and `neutral`; and code's), each `#rrggbb`. | ||
They are named by purpose, never by hue: a theme may draw success in blue, | ||
for those who cannot tell red from green, and `green` would then lie. The | ||
hue names are the pages' own tokens, which the palette is mapped onto | ||
docs/guide/reviews.md+1−1
| @@ -1,6 +1,6 @@ | ||
# Review agents | ||
| ||
`knotless-agent review` reviews your pulls with a model. You ask its account for a review on a pull, and it reads the pull, writes a review with a verdict, a body and comments on lines, and reviews again when you push. It works as GitHub's review bots do, as an atproto account of its own. It ships with `knotless-claude-review`, which has Claude Code do the reading, and this guide sets that up; any other model works through [another command](#use-another-command). | ||
`knotless-agent review` reviews your pulls with a model. You ask its account for a review on a pull, and it reads the pull, writes a review with a verdict, a body and comments on lines, and reviews again when you push. It is an atproto account of its own. It ships with `knotless-claude-review`, which has Claude Code do the reading, and this guide sets that up; any other model works through [another command](#use-another-command). | ||
| ||
## What you need | ||
| ||
harness/e2e/flows.spec.mjs+25−4
| @@ -160,7 +160,7 @@ test('opening a pull from a fork', async () => { | ||
await page.getByLabel('From').selectOption({ label: 'bob.test/tally (yours)' }) | ||
await page.getByLabel('Branch').selectOption('main') | ||
await expect(page.locator('.compared')).toContainText('Release from tags when asked') | ||
// The form unfolds once asked for, as GitHub's compare page does. | ||
// The form unfolds once asked for. | ||
await expect(page.getByRole('textbox', { name: 'Title' })).toHaveCount(0) | ||
await page.locator('.compare-ready').getByRole('button', { name: 'Open pull' }).click() | ||
await page.getByRole('textbox', { name: 'Title' }).fill('Bring in the fork: usage, totals and releases') | ||
| @@ -169,15 +169,20 @@ test('opening a pull from a fork', async () => { | ||
await page.locator('.reviewer-menu').getByRole('button', { name: 'alice.test' }).click() | ||
await page.locator('.reviewer-menu summary').click() | ||
await expect(page.locator('.side-people')).toContainText('alice.test') | ||
await page.getByRole('button', { name: 'Open pull' }).click() | ||
// Opened as a draft. | ||
await page.getByRole('button', { name: 'Open as draft' }).click() | ||
await page.waitForURL(/\/alice\.test\/tally\/pull\//) | ||
proposed = page.url() | ||
await expect(page.getByRole('heading', { name: 'Bring in the fork: usage, totals and releases' })).toBeVisible() | ||
const pulls = await records('bob', 'dev.knotless.pull') | ||
const made = pulls.find((r) => r.value.title === 'Bring in the fork: usage, totals and releases') | ||
await expect(page.locator('.item-meta .pill')).toHaveText('Draft') | ||
const made = await fork() | ||
expect(made?.value.reviewers?.map((r) => r.did)).toContain(who.alice.did) | ||
expect(made?.value.draft).toBe(true) | ||
}) | ||
| ||
/** bob's pull from his fork, as his PDS holds it. */ | ||
const fork = async () => (await records('bob', 'dev.knotless.pull')).find((r) => r.value.title === 'Bring in the fork: usage, totals and releases') | ||
| ||
test('retargeting a pull', async () => { | ||
await go(page, proposed) | ||
await page.locator('.merge-box .more-menu summary').click() | ||
| @@ -190,6 +195,22 @@ test('retargeting a pull', async () => { | ||
await anew(page, (expect) => expect(page.getByText(/wants to merge into main from bob\.test\/tally:words$/)).toBeVisible()) | ||
}) | ||
| ||
test('marking a draft ready, and making it a draft again', async () => { | ||
await go(page, proposed) | ||
const box = page.locator('.merge-box') | ||
await box.getByRole('button', { name: 'Ready for review' }).click() | ||
await expect(page.locator('.item-meta .pill')).toHaveText('Open') | ||
expect((await fork())?.value.draft).toBeFalsy() | ||
await box.locator('.more-menu summary').click() | ||
await box.getByRole('button', { name: 'Convert to draft' }).click() | ||
await expect(page.locator('.item-meta .pill')).toHaveText('Draft') | ||
await expect(box.getByText('This pull is still a draft')).toBeVisible() | ||
expect((await fork())?.value.draft).toBe(true) | ||
// Ready once more, for what follows. | ||
await box.getByRole('button', { name: 'Ready for review' }).click() | ||
await anew(page, (expect) => expect(page.locator('.item-meta .pill')).toHaveText('Open')) | ||
}) | ||
| ||
test('rerunning a check', async () => { | ||
// bob runs his fork's checks: the failed test, asked for again. | ||
await go(page, world.views.check) | ||
web/src/lib/organisms/MergeBox.svelte+13−5
| @@ -31,13 +31,14 @@ | ||
* a branch that takes pulls only through the queue, there is no merge | ||
* button: the queue lands it. A `draft` neither merges nor queues | ||
* until its author marks it ready, here or with `knotless edit | ||
* --ready`; those it `requested` to review it show with the reviews. | ||
* --ready`, and its author makes one a draft again from the More | ||
* menu; those it `requested` to review it show with the reviews. | ||
*/ | ||
let { repo, pull, base, head, queue, reviews = [], requested = [], draft = false, checks, minutes = [], rerun, in: logged }: { repo: RepoViewDetailed; pull: string; base: string; head?: PullHeadView; queue?: QueueStateView; reviews?: ReviewView[]; requested?: ActorView[]; draft?: boolean; checks: CheckView[]; minutes?: MinutesView[]; rerun?: Snippet; in: boolean } = $props(); | ||
type Status = Answer<typeof getMergeStatus.main>; | ||
let status = $state<Status | null>(null); | ||
let problem = $state(''); | ||
// Which is under way: 'merge', 'queue', 'leave', 'ready' or 'retarget'. | ||
// Which is under way: 'merge', 'queue', 'leave', 'ready', 'draft' or 'retarget'. | ||
let busy = $state(''); | ||
let keep = $state(false); | ||
let merged = $state(''); | ||
| @@ -82,7 +83,8 @@ | ||
}); | ||
const enqueue = () => doing('queue', () => call<typeof queuePull.main>('dev.knotless.queuePull', one)); | ||
const leave = () => doing('leave', () => call<typeof leaveQueue.main>('dev.knotless.leaveQueue', one)); | ||
const ready = () => doing('ready', () => call<typeof editItem.main>('dev.knotless.editItem', { repo: repo.uri, subject: pull as AtUriString, draft: false })); | ||
/** Marks it ready for review, or with `again` a draft once more. */ | ||
const ready = (again = false) => doing(again ? 'draft' : 'ready', () => call<typeof editItem.main>('dev.knotless.editItem', { repo: repo.uri, subject: pull as AtUriString, draft: again })); | ||
/** Points it at the branches the form names, those that changed. */ | ||
async function point(e: SubmitEvent) { | ||
e.preventDefault(); | ||
| @@ -164,7 +166,7 @@ | ||
<div class="row draft"> | ||
<Icon name="pullDraft" /> | ||
<div class="grow"><b>This pull is still a draft</b><p class="mute">{s.may.head ? 'It cannot be merged or queued until you mark it ready for review.' : 'It cannot be merged or queued until its author marks it ready for review.'}</p></div> | ||
{#if s.may.head}<button class="btn" type="button" disabled={!!busy} aria-busy={busy === 'ready'} onclick={ready}>Ready for review</button>{/if} | ||
{#if s.may.head}<button class="btn" type="button" disabled={!!busy} aria-busy={busy === 'ready'} onclick={() => ready()}>{busy === 'ready' ? 'Marking ready…' : 'Ready for review'}</button>{/if} | ||
</div> | ||
{/if} | ||
{@const first = queueFirst(s) && !draft} | ||
| @@ -183,7 +185,13 @@ | ||
{#if s.may.base || s.may.head} | ||
<details class="popover more-menu end"> | ||
<summary class="btn" aria-label="More"><Icon name="kebab" /></summary> | ||
<div class="menu"><button class="menu-item" type="button" popovertarget={retarget}>Retarget…</button></div> | ||
<div class="menu"> | ||
<button class="menu-item" type="button" popovertarget={retarget}><Icon name="branch" />Retarget…</button> | ||
{#if s.may.head && !draft}<button class="menu-item" type="button" disabled={!!busy} aria-busy={busy === 'draft'} onclick={(e) => { | ||
e.currentTarget.closest('details')?.removeAttribute('open'); | ||
ready(true); | ||
}}><Icon name="pullDraft" />Convert to draft</button>{/if} | ||
</div> | ||
</details> | ||
{/if} | ||
</div> | ||
web/src/lib/organisms/OpenForm.svelte+12−7
| @@ -2,25 +2,29 @@ | ||
import type { Image } from '#lib/lexicons/dev/knotless/defs.ts'; | ||
import { untrack } from 'svelte'; | ||
import type { AtUriString } from '@atproto/lex'; | ||
import Icon from '#lib/atoms/Icon.svelte'; | ||
import MarkdownField from '#lib/molecules/MarkdownField.svelte'; | ||
/** | ||
* Opening a pull or an issue: its title, from `title`, and its body in | ||
* Markdown, from `text`, as the repository's template asks. `send` is | ||
* given both, with the pictures uploaded into the body, and answers why it failed, in the server's words, or | ||
* nothing. Its preview resolves relative links inside `repo`. Anyone | ||
* not `in` has no button to send it. | ||
* not `in` has no button to send it. With `draft`, a second button | ||
* of that name sends it as a draft. | ||
*/ | ||
let { in: logged, label, title = '', text = '', send, repo }: { in: boolean; label: string; title?: string; text?: string; send: (title: string, body: string, images?: Image[]) => Promise<string | undefined>; repo?: AtUriString } = $props(); | ||
let { in: logged, label, draft, title = '', text = '', send, repo }: { in: boolean; label: string; draft?: string; title?: string; text?: string; send: (title: string, body: string, images?: Image[], draft?: boolean) => Promise<string | undefined>; repo?: AtUriString } = $props(); | ||
let heading = $state(untrack(() => title)); | ||
let body = $state(untrack(() => text)); | ||
let images = $state<Image[]>([]); | ||
let busy = $state(false); | ||
/** Which button sent it, while it is under way: 'open' or 'draft'. */ | ||
let busy = $state(''); | ||
let problem = $state(''); | ||
async function submit(e: SubmitEvent) { | ||
e.preventDefault(); | ||
busy = true; | ||
problem = (await send(heading, body, images.length ? images : undefined)) ?? ''; | ||
busy = false; | ||
const drafted = (e.submitter as HTMLButtonElement | null)?.value === 'draft'; | ||
busy = drafted ? 'draft' : 'open'; | ||
problem = (await send(heading, body, images.length ? images : undefined, drafted)) ?? ''; | ||
busy = ''; | ||
} | ||
</script> | ||
| ||
| @@ -29,6 +33,7 @@ | ||
<MarkdownField label="Description" placeholder="What it is about, in Markdown" rows={14} {repo} bind:value={body} bind:images /> | ||
{#if problem}<p class="problem" role="alert">{problem}</p>{/if} | ||
<div class="composer-end"> | ||
{#if logged}<button class="btn primary" type="submit" disabled={busy || !heading.trim()} aria-busy={busy}>{label}</button>{/if} | ||
{#if logged && draft}<button class="btn" type="submit" value="draft" disabled={!!busy || !heading.trim()} aria-busy={busy === 'draft'}><Icon name="pullDraft" />{draft}</button>{/if} | ||
{#if logged}<button class="btn primary" type="submit" value="open" disabled={!!busy || !heading.trim()} aria-busy={busy === 'open'}>{label}</button>{/if} | ||
</div> | ||
</form> | ||
web/src/lib/organisms/OpenSide.svelte+1−1
| @@ -7,7 +7,7 @@ | ||
import Person from '#lib/molecules/Person.svelte'; | ||
import ReviewerPicker from '#lib/molecules/ReviewerPicker.svelte'; | ||
/** | ||
* Beside the form that opens a pull or an issue, as GitHub's is: the | ||
* Beside the form that opens a pull or an issue: the | ||
* labels to put on it, of those `offered`, and with `people` the | ||
* reviewers to ask, from them. What is chosen is bound, `labels` by | ||
* name and `reviewers` by DID, for the form to send as it opens. | ||
web/src/lib/organisms/SettingsLabels.svelte+1−1
| @@ -11,7 +11,7 @@ | ||
/** | ||
* 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, | ||
* as GitHub's are, and taken away, as `knotless labels add` and | ||
* and taken away, as `knotless labels add` and | ||
* `remove` do. | ||
*/ | ||
let { uri, labels }: { uri: AtUriString; labels: LabelDef[] } = $props(); | ||
web/src/routes/[authority]/[repo]/pulls/new/+page.svelte+4−4
| @@ -22,7 +22,7 @@ | ||
const people = $derived([r.owner, ...(r.collaborators ?? [])].filter((p) => p.did !== me)); | ||
let labels = $state<string[]>([]); | ||
let reviewers = $state<(typeof r.owner.did)[]>([]); | ||
/** Whether the form to open it shows: once asked for, as GitHub's compare page unfolds it, or at once from Compare & pull. */ | ||
/** Whether the form to open it shows: once asked for, or at once from Compare & pull. */ | ||
let asked = $state(false); | ||
const writing = $derived(asked || data.expand); | ||
const uriOf = (h: { did: string; name: string }) => `at://${h.did}/dev.knotless.repo/${h.name}`; | ||
| @@ -37,8 +37,8 @@ | ||
if (e.currentTarget.name === 'head') (form.elements.namedItem('branch') as HTMLSelectElement).value = ''; | ||
form.requestSubmit(); | ||
} | ||
async function open(title: string, body: string, images?: Image[]) { | ||
const input = { repo: r.uri, base: data.base, head: data.head, branch: data.branch, title, body: body || undefined, images, labels: labels.length ? labels.map((name) => ({ name })) : undefined, reviewers: reviewers.length ? reviewers.map((did) => ({ did })) : undefined }; | ||
async function open(title: string, body: string, images?: Image[], draft?: boolean) { | ||
const input = { repo: r.uri, base: data.base, head: data.head, branch: data.branch, title, body: body || undefined, images, draft: draft || undefined, labels: labels.length ? labels.map((name) => ({ name })) : undefined, reviewers: reviewers.length ? reviewers.map((did) => ({ did })) : undefined }; | ||
const opened = await call<typeof createPull.main>('dev.knotless.createPull', input).catch((e: Error) => e); | ||
if (opened instanceof Error) return opened.message; | ||
await goto(itemHref(base, opened.uri)); | ||
| @@ -73,7 +73,7 @@ | ||
{#if writing} | ||
<div class="open-layout"> | ||
{#key `${data.head} ${data.branch} ${data.base}`} | ||
<OpenForm repo={`at://${data.authority}/dev.knotless.repo/${r.name}`} in={!!me} label="Open pull" {title} text={data.template} send={open} /> | ||
<OpenForm repo={`at://${data.authority}/dev.knotless.repo/${r.name}`} in={!!me} label="Open pull" draft="Open as draft" {title} text={data.template} send={open} /> | ||
{/key} | ||
{#if me}<OpenSide {offered} bind:labels {people} bind:reviewers />{/if} | ||
</div> | ||