204 lines
7.5 KiB
JavaScript
204 lines
7.5 KiB
JavaScript
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();
|