/** * Git Board — Surface Entry Point * * Kanban board for Gitea issues and PRs. * Calls Starlark backend via /s/git-board/api/*. * Authentication via gitea-client library connections. * * Features: DnD between columns, issue detail modal with comments. */ (async function () { 'use strict'; var mount = document.getElementById('extension-mount'); if (!mount) return; var base = window.__BASE__ || ''; var ver = window.__VERSION__ || '0'; var slug = 'git-board'; // ── Boot SDK ─────────────────────────────── try { if (!window.preact) { var { h, render } = await import(base + '/js/sw/vendor/preact.module.js'); var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js'); var htmModule = await import(base + '/js/sw/vendor/htm.module.js'); window.preact = { h, render }; window.hooks = hooksModule; window.html = htmModule.default.bind(h); } var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver); await sdk.boot(); } catch (e) { mount.innerHTML = '
SDK boot failed: ' + e.message + '
'; return; } var { html } = window; var { useState, useEffect, useCallback, useRef, useMemo } = hooks; var { render } = preact; var API = base + '/s/' + slug + '/api'; async function api(path, opts) { var token = sw.auth._getToken(); var fetchOpts = { headers: { 'Authorization': 'Bearer ' + token } }; if (opts && opts.method) fetchOpts.method = opts.method; if (opts && opts.body) { fetchOpts.body = JSON.stringify(opts.body); fetchOpts.headers['Content-Type'] = 'application/json'; } var resp = await fetch(API + path, fetchOpts); var text = await resp.text(); try { return JSON.parse(text); } catch (_) { return { error: text }; } } // ── App ──────────────────────────────────── function App() { var [owner, setOwner] = useState(''); var [repo, setRepo] = useState(''); var [repos, setRepos] = useState([]); var [board, setBoard] = useState(null); var [loading, setLoading] = useState(false); var [needsConn, setNeedsConn] = useState(false); var [modal, setModal] = useState(null); // {owner, repo, number} var menuRef = useRef(null); useEffect(function () { if (menuRef.current && sw.userMenu) { sw.userMenu(menuRef.current, { placement: 'down-left' }); } }, []); useEffect(function () { api('/repos').then(function (d) { if (d.error) { if (d.error.indexOf('no gitea connection configured') !== -1) { setNeedsConn(true); } else { sw.toast(d.error, 'error'); } return; } setRepos(d.data || []); if (d.data && d.data.length > 0 && !owner) { setOwner(d.data[0].owner); setRepo(d.data[0].name); } }); }, []); var loadBoard = useCallback(function () { if (!owner || !repo) return; setLoading(true); api('/board?owner=' + encodeURIComponent(owner) + '&repo=' + encodeURIComponent(repo)) .then(function (d) { if (d.error) { sw.toast(d.error, 'error'); if (d.error.indexOf('no gitea connection configured') !== -1) setNeedsConn(true); } else { setBoard(d); setNeedsConn(false); } setLoading(false); }); }, [owner, repo]); useEffect(function () { loadBoard(); }, [loadBoard]); var onDrop = useCallback(function (issueNumber, targetCol) { if (!board) return; var issue = (board.issues || []).find(function (i) { return i.number === issueNumber; }); if (!issue) return; var patch = {}; if (targetCol === 'open') { // Move to Open: unassign + reopen if (issue.state === 'closed') patch.state = 'open'; // Note: Gitea PATCH /issues doesn't support clearing assignee via empty string, // but we do our best — the board will re-split on refresh. } else if (targetCol === 'in_progress') { // Move to In Progress: assign to current user + ensure open if (issue.state === 'closed') patch.state = 'open'; // Gitea needs assignees array — not supported by our simple update_issue. // For now, just reopen. User assigns via modal. if (issue.state === 'closed') patch.state = 'open'; } else if (targetCol === 'done') { patch.state = 'closed'; } if (!patch.state) return; // no meaningful change // Optimistic update var newIssues = (board.issues || []).map(function (i) { if (i.number !== issueNumber) return i; var copy = {}; for (var k in i) copy[k] = i[k]; if (patch.state) copy.state = patch.state; return copy; }); setBoard({ issues: newIssues, pull_requests: board.pull_requests || [] }); api('/issue/' + owner + '/' + repo + '/' + issueNumber, { method: 'POST', body: patch }).then(function (d) { if (d.error) { sw.toast('Update failed: ' + d.error, 'error'); loadBoard(); // revert } }); }, [board, owner, repo, loadBoard]); var openModal = useCallback(function (number) { setModal({ owner: owner, repo: repo, number: number }); }, [owner, repo]); var closeModal = useCallback(function (changed) { setModal(null); if (changed) loadBoard(); }, [loadBoard]); return html` ${needsConn ? html`<${ConnectionSetup} />` : html`Git Board requires a Gitea connection to fetch repositories, issues, and pull requests.
Ask your admin to add a Gitea connection in Admin → Connections, or add a personal one in Settings → Connections.
Connections are managed centrally — no API tokens in extension settings.
Comments (${(issue.comments || []).length})
${(issue.comments || []).length === 0 && html`No comments yet.
`} ${(issue.comments || []).map(function (c, i) { return html`