// — open pull requests for the selected repo, with "Merge & clean up". // // A self-contained control (AGENT.md §1.1): shadow DOM, self-fetching, cleans up // on disconnect. It listens for `repo:select` and shows the repo's open PRs when // a forge (Gitea) is configured; otherwise it stays hidden (graceful, §8.4). // "Merge & clean up" is a DESTRUCTIVE action (§1.4): it confirms — naming the PR, // base, and branch to be deleted — before calling the server. class PRList extends HTMLElement { #controller = null; #onSelect = null; #path = ''; constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.#renderShell(); this.#onSelect = (e) => this.#load(e.detail?.path); document.addEventListener('repo:select', this.#onSelect); } disconnectedCallback() { document.removeEventListener('repo:select', this.#onSelect); this.#controller?.abort(); } async #load(path) { if (!path) return; this.#path = path; this.#controller?.abort(); this.#controller = new AbortController(); try { const res = await fetch(`/api/repo/prs?path=${encodeURIComponent(path)}`, { signal: this.#controller.signal }); const data = await res.json(); if (!data.supported) { this.hidden = true; return; } this.hidden = false; this.#render(data.prs || []); } catch (err) { if (err.name !== 'AbortError') { this.hidden = false; this.#error(err.message); } } } async #merge(pr) { const ok = window.confirm( `Merge & clean up PR #${pr.number}: "${pr.title}"?\n\n` + `This squash-merges it into ${pr.base} and DELETES the branch "${pr.head}".\n` + `This cannot be undone.` ); if (!ok) return; try { const res = await fetch('/api/repo/pr/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: this.#path, number: pr.number }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); this.#load(this.#path); // refresh the list } catch (err) { this.#error(`Merge failed: ${err.message}`); } } #render(prs) { const body = this.shadowRoot.getElementById('body'); if (prs.length === 0) { body.innerHTML = `

No open pull requests.

`; return; } body.replaceChildren(...prs.map((pr) => { const li = document.createElement('li'); li.innerHTML = `
#${pr.number} ${this.#esc(pr.title)} ${pr.draft ? `draft` : ''}
${this.#esc(pr.author)} · ${this.#esc(pr.head)}${this.#esc(pr.base)} ${pr.sameRepo ? '' : `fork`}
`; const btn = document.createElement('button'); btn.textContent = 'Merge & clean up'; btn.className = 'merge'; btn.title = pr.draft ? 'This PR is a draft' : 'Squash-merge and delete the branch'; btn.onclick = () => this.#merge(pr); li.querySelector('.row').appendChild(btn); return li; })); } #error(msg) { this.shadowRoot.getElementById('body').innerHTML = `

${this.#esc(msg)}

`; } #renderShell() { this.shadowRoot.innerHTML = `

Pull requests

Select a repository.

`; } #esc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ( { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] )); } } customElements.define('pr-list', PRList);