function createController({request, storage, login, createId = () => globalThis.crypto?.randomUUID?.() || String(Date.now())}) { const storageKey = 'stackchain.create-pull.v1.' + String(login || '').toLowerCase(); let operationId = createId(); let current = { repository:'', branches:[], head:'', base:'', expected_head_sha:'', title:'', body:'', draft:true, }; try { const saved = JSON.parse(storage?.getItem?.(storageKey) || 'null'); if (saved && typeof saved === 'object') { for (const field of ['repository', 'head', 'base', 'title', 'body']) { if (typeof saved[field] === 'string') current[field] = saved[field]; } if (typeof saved.draft === 'boolean') current.draft = saved.draft; } } catch (_) {} function snapshot() { return JSON.parse(JSON.stringify(current)); } function persist() { storage?.setItem?.(storageKey, JSON.stringify({ repository:current.repository, head:current.head, base:current.base, title:current.title, body:current.body, draft:current.draft, })); } async function selectRepository(repository) { const options = await request('api/v1/repos/' + repository + '/pull-creation-options'); const branches = Array.isArray(options?.branches) ? options.branches : []; const base = branches.some(branch => branch.name === options.default_branch) ? options.default_branch : (branches[0]?.name || ''); const headBranch = branches.find(branch => branch.name !== base) || branches[0] || {}; current = { ...current, repository, branches, base, head:headBranch.name || '', expected_head_sha:headBranch.sha || '', }; persist(); return snapshot(); } function update(values) { current = {...current, ...values}; if (Object.prototype.hasOwnProperty.call(values, 'head')) { current.expected_head_sha = current.branches.find(branch => branch.name === values.head)?.sha || ''; } persist(); return snapshot(); } async function submit() { const result = await request('api/v1/repos/' + current.repository + '/pulls', { method:'POST', headers:{'Idempotency-Key':operationId}, body:{ head:current.head, base:current.base, title:current.title, body:current.body, draft:current.draft, expected_head_sha:current.expected_head_sha, }, }); operationId = createId(); return result; } return { state:snapshot, selectRepository, update, submit, }; } function createBinding({controller, render, status, onCreated}) { return { async repositoryChanged(repository) { status('Loading branches…'); try { const state = await controller.selectRepository(repository); render(state); status(state.branches.length ? 'Choose the source and base branches.' : 'No branches are available.'); return state; } catch (error) { status(error.message || 'Branches could not be loaded.'); throw error; } }, changed(values) { const state = controller.update(values); render(state); return state; }, async submit() { status('Creating pull request…'); try { const result = await controller.submit(); status(result.existing ? `Pull request #${result.number} is already open.` : `Pull request #${result.number} created.`); onCreated(result); return result; } catch (error) { status(error.message || 'The pull request could not be created. Your draft is safe.'); throw error; } }, }; } async function bindDashboard() { const qs = selector => document.querySelector(selector); const switchButton = qs('#switch-to-create-pull'); switchButton.disabled = true; const identityResponse = await fetch('api/v1/background-identity').catch(()=>null); const identity = await identityResponse?.json().catch(()=>({})); if (!identityResponse?.ok || !identity?.login) { switchButton.title = 'Account identity is unavailable.'; return null; } const login = identity.login; const root = qs('#create-pull-sheet'); const repository = qs('#create-pull-repository'); const head = qs('#create-pull-head'); const base = qs('#create-pull-base'); const title = qs('#create-pull-title'); const body = qs('#create-pull-body'); const submit = qs('#submit-create-pull'); const request = async (url, options) => { const response = await fetch(url, options ? { method:options.method, headers:{'Content-Type':'application/json', ...options.headers}, body:JSON.stringify(options.body), } : undefined); const payload = await response.json().catch(()=>({})); if (!response.ok) throw new Error(payload.error || payload.detail || 'Request failed.'); return payload; }; const controller = createController({request, storage:localStorage, login}); const setOptions = (select, branches) => { select.replaceChildren(...branches.map(branch => { const option = document.createElement('option'); option.value = option.textContent = branch.name; return option; })); }; const render = state => { setOptions(head, state.branches); setOptions(base, state.branches); head.value = state.head; base.value = state.base; head.disabled = base.disabled = !state.branches.length; qs('#create-pull-head-receipt').textContent = state.expected_head_sha ? 'Source at ' + state.expected_head_sha.slice(0, 12) : ''; submit.disabled = !(state.repository && state.head && state.base && state.head !== state.base && title.value.trim()); }; const binding = createBinding({ controller, render, status:message=>{ qs('#create-pull-status').textContent = message; }, onCreated:result=>{ root.hidden = true; qs('main').inert = false; location.hash = '#/my-work/pull/' + result.repository + '/' + result.number; location.reload(); }, }); const update = () => binding.changed({ head:head.value, base:base.value, title:title.value, body:body.value, draft:document.querySelector('input[name="create-pull-mode"]:checked')?.value !== 'ready', }); repository.addEventListener('change', () => binding.repositoryChanged(repository.value).catch(()=>{})); [head,base,title,body].forEach(field => field.addEventListener('input', update)); document.querySelectorAll('input[name="create-pull-mode"]').forEach(field => field.addEventListener('change', () => { update(); submit.textContent = field.value === 'ready' && field.checked ? 'Create ready pull request' : 'Create draft pull request'; })); qs('#create-pull-form').addEventListener('submit', event => { event.preventDefault(); update(); binding.submit().catch(()=>{}); }); const close = () => { root.hidden = true; qs('main').inert = false; qs('#new-issue').focus(); }; qs('#cancel-create-pull').addEventListener('click', close); switchButton.addEventListener('click', () => { qs('#cancel-new-issue').click(); repository.replaceChildren(...Array.from(qs('#create-issue-repository').options).map(source => { const option = document.createElement('option'); option.value = source.value; option.textContent = source.textContent; return option; })); root.hidden = false; qs('main').inert = true; qs('#cancel-create-pull').focus(); }); root.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); close(); } }); switchButton.disabled = false; return binding; } const cp = {createController, createBinding, b:bindDashboard}; if (typeof module !== 'undefined') module.exports = cp; else cp.b();