From 7a593d082acdd198dc47c3dd575f49725cbc20e0 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Fri, 13 Mar 2026 13:59:27 +0000 Subject: [PATCH] feat: assignee field in feedback widget - New GET /api/collaborators?repo= endpoint fetches repo owner + collabs - Assignee dropdown added to the new issue form (lazy-loaded on first open) - Selected assignee is forwarded to Gitea via the assignees[] field - Dropdown resets to Unassigned after successful submission Closes #5 --- server.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 63cf2f2..394f1c3 100644 --- a/server.js +++ b/server.js @@ -71,14 +71,33 @@ app.get(['/api/issues', BASE + '/api/issues'], async (req, res) => { } catch (e) { res.status(500).json({ error: e.message }); } }); +// GET /api/collaborators?repo=owner/repo +app.get(['/api/collaborators', BASE + '/api/collaborators'], async (req, res) => { + const { repo } = req.query; + if (!repo) return res.status(400).json({ error: 'repo required' }); + try { + const users = await giteaRequest('GET', `/api/v1/repos/${repo}/collaborators?limit=50`); + // Also include the owner + const repoInfo = await giteaRequest('GET', `/api/v1/repos/${repo}`); + const owner = repoInfo.owner ? [{ login: repoInfo.owner.login }] : []; + const collabs = Array.isArray(users) ? users.map(u => ({ login: u.login })) : []; + // Dedupe + const seen = new Set(); + const all = [...owner, ...collabs].filter(u => { if (seen.has(u.login)) return false; seen.add(u.login); return true; }); + res.json(all); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + // POST /api/feedback β€” create new issue app.post(['/api/feedback', BASE + '/api/feedback'], async (req, res) => { - const { title, body, repo, source_url } = req.body; + const { title, body, repo, source_url, assignee } = req.body; if (!title) return res.status(400).json({ error: 'title required' }); if (!repo) return res.status(400).json({ error: 'repo required' }); const issueBody = [body || '', '', '---', `*Submitted via feedback widget from: ${source_url || 'unknown'}*`].join('\n'); + const payload = { title, body: issueBody }; + if (assignee) payload.assignees = [assignee]; try { - const issue = await giteaRequest('POST', `/api/v1/repos/${repo}/issues`, { title, body: issueBody }); + const issue = await giteaRequest('POST', `/api/v1/repos/${repo}/issues`, payload); if (issue.number) res.json({ ok: true, issue_number: issue.number, issue_url: issue.html_url }); else res.status(500).json({ error: 'Failed', raw: JSON.stringify(issue).slice(0, 200) }); } catch (e) { res.status(500).json({ error: e.message }); } @@ -151,6 +170,14 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { .fb-issue-arrow { color:#444; font-size:.8rem; flex-shrink:0; } .fb-empty { color:#555; font-size:.85rem; text-align:center; padding:1.5rem 0; } .fb-loading { color:#555; font-size:.85rem; text-align:center; padding:1rem 0; } + /* Assignee select */ + .fb-select { background:#0d0d0d; border:1px solid #2a2a2a; border-radius:8px; + padding:.6rem .8rem; color:#e0e0e0; font-size:.875rem; width:100%; font-family:inherit; + outline:none; transition:border-color .15s; appearance:none; + background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat:no-repeat; background-position:right .7rem center; padding-right:2rem; } + .fb-select:focus { border-color:#7c9ef5; } + .fb-field-label { color:#666; font-size:.75rem; margin-bottom:.2rem; } /* Picker row inside new-issue form */ .fb-picker-row { display:flex; align-items:center; gap:.6rem; } .fb-picker-btn { background:#1a2a1a; border:1px solid #2a4a2a; color:#6fcf6f; font-size:.78rem; @@ -205,6 +232,12 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
+
+
Assignee
+ +
Click to reference a page element @@ -224,8 +257,26 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { \`; document.body.appendChild(overlay); + // ── Load collaborators into assignee dropdown ───────────────────────────── + var assigneesLoaded = false; + function loadAssignees() { + if (assigneesLoaded || !cfg.repo) return; + assigneesLoaded = true; + fetch(SELF + '/api/collaborators?repo=' + encodeURIComponent(cfg.repo)) + .then(function(r) { return r.json(); }) + .then(function(users) { + var sel = document.getElementById('_fb-assignee'); + if (!Array.isArray(users)) return; + users.forEach(function(u) { + var opt = document.createElement('option'); + opt.value = u.login; opt.textContent = u.login; + sel.appendChild(opt); + }); + }).catch(function() {}); + } + // ── Open / close ────────────────────────────────────────────────────────── - function openOverlay() { overlay.classList.add('open'); } + function openOverlay() { overlay.classList.add('open'); loadAssignees(); } function closeOverlay() { overlay.classList.remove('open'); } fab.addEventListener('click', function() { @@ -347,19 +398,21 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => { document.getElementById('_fb-submit').addEventListener('click', function() { var title = document.getElementById('_fb-title').value.trim(); var body = document.getElementById('_fb-desc').value.trim(); + var assignee = document.getElementById('_fb-assignee').value; var status = document.getElementById('_fb-status'); var btn = document.getElementById('_fb-submit'); if (!title) { status.style.color='#f57c7c'; status.textContent='Title is required.'; return; } btn.disabled = true; status.style.color='#888'; status.textContent='Submitting…'; fetch(SELF + '/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title, body, repo: cfg.repo, source_url: window.location.href }) + body: JSON.stringify({ title, body, repo: cfg.repo, source_url: window.location.href, assignee: assignee || undefined }) }).then(function(r) { return r.json(); }).then(function(d) { if (d.ok) { status.style.color='#6fcf6f'; status.textContent='βœ“ Issue #' + d.issue_number + ' created!'; document.getElementById('_fb-title').value = ''; document.getElementById('_fb-desc').value = ''; + document.getElementById('_fb-assignee').value = ''; document.getElementById('_fb-snippet-wrap').style.display = 'none'; document.getElementById('_fb-pick-hint').textContent = 'Click to reference a page element'; capturedSnippet = '';