feat: assignee field in feedback widget
All checks were successful
Deploy to dev.bl.pixeldev.eu / deploy (push) Successful in 2s
All checks were successful
Deploy to dev.bl.pixeldev.eu / deploy (push) Successful in 2s
- 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
This commit is contained in:
parent
be8c814da6
commit
7a593d082a
61
server.js
61
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 }); }
|
} 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
|
// POST /api/feedback — create new issue
|
||||||
app.post(['/api/feedback', BASE + '/api/feedback'], async (req, res) => {
|
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 (!title) return res.status(400).json({ error: 'title required' });
|
||||||
if (!repo) return res.status(400).json({ error: 'repo 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 issueBody = [body || '', '', '---', `*Submitted via feedback widget from: ${source_url || 'unknown'}*`].join('\n');
|
||||||
|
const payload = { title, body: issueBody };
|
||||||
|
if (assignee) payload.assignees = [assignee];
|
||||||
try {
|
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 });
|
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) });
|
else res.status(500).json({ error: 'Failed', raw: JSON.stringify(issue).slice(0, 200) });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} 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-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-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; }
|
.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 */
|
/* Picker row inside new-issue form */
|
||||||
.fb-picker-row { display:flex; align-items:center; gap:.6rem; }
|
.fb-picker-row { display:flex; align-items:center; gap:.6rem; }
|
||||||
.fb-picker-btn { background:#1a2a1a; border:1px solid #2a4a2a; color:#6fcf6f; font-size:.78rem;
|
.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) => {
|
|||||||
<div style="display:flex;flex-direction:column;gap:.8rem;">
|
<div style="display:flex;flex-direction:column;gap:.8rem;">
|
||||||
<input class="fb-input" id="_fb-title" type="text" placeholder="Title (required)" />
|
<input class="fb-input" id="_fb-title" type="text" placeholder="Title (required)" />
|
||||||
<textarea class="fb-textarea" id="_fb-desc" placeholder="Describe your feedback…"></textarea>
|
<textarea class="fb-textarea" id="_fb-desc" placeholder="Describe your feedback…"></textarea>
|
||||||
|
<div>
|
||||||
|
<div class="fb-field-label">Assignee</div>
|
||||||
|
<select class="fb-select" id="_fb-assignee">
|
||||||
|
<option value="">— Unassigned —</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="fb-picker-row">
|
<div class="fb-picker-row">
|
||||||
<button class="fb-picker-btn" id="_fb-pick-btn">🎯 Pick element</button>
|
<button class="fb-picker-btn" id="_fb-pick-btn">🎯 Pick element</button>
|
||||||
<span class="fb-picker-hint" id="_fb-pick-hint">Click to reference a page element</span>
|
<span class="fb-picker-hint" id="_fb-pick-hint">Click to reference a page element</span>
|
||||||
@ -224,8 +257,26 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
\`;
|
\`;
|
||||||
document.body.appendChild(overlay);
|
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 ──────────────────────────────────────────────────────────
|
// ── Open / close ──────────────────────────────────────────────────────────
|
||||||
function openOverlay() { overlay.classList.add('open'); }
|
function openOverlay() { overlay.classList.add('open'); loadAssignees(); }
|
||||||
function closeOverlay() { overlay.classList.remove('open'); }
|
function closeOverlay() { overlay.classList.remove('open'); }
|
||||||
|
|
||||||
fab.addEventListener('click', function() {
|
fab.addEventListener('click', function() {
|
||||||
@ -347,19 +398,21 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
document.getElementById('_fb-submit').addEventListener('click', function() {
|
document.getElementById('_fb-submit').addEventListener('click', function() {
|
||||||
var title = document.getElementById('_fb-title').value.trim();
|
var title = document.getElementById('_fb-title').value.trim();
|
||||||
var body = document.getElementById('_fb-desc').value.trim();
|
var body = document.getElementById('_fb-desc').value.trim();
|
||||||
|
var assignee = document.getElementById('_fb-assignee').value;
|
||||||
var status = document.getElementById('_fb-status');
|
var status = document.getElementById('_fb-status');
|
||||||
var btn = document.getElementById('_fb-submit');
|
var btn = document.getElementById('_fb-submit');
|
||||||
if (!title) { status.style.color='#f57c7c'; status.textContent='Title is required.'; return; }
|
if (!title) { status.style.color='#f57c7c'; status.textContent='Title is required.'; return; }
|
||||||
btn.disabled = true; status.style.color='#888'; status.textContent='Submitting…';
|
btn.disabled = true; status.style.color='#888'; status.textContent='Submitting…';
|
||||||
fetch(SELF + '/api/feedback', {
|
fetch(SELF + '/api/feedback', {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
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) {
|
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||||
if (d.ok) {
|
if (d.ok) {
|
||||||
status.style.color='#6fcf6f';
|
status.style.color='#6fcf6f';
|
||||||
status.textContent='✓ Issue #' + d.issue_number + ' created!';
|
status.textContent='✓ Issue #' + d.issue_number + ' created!';
|
||||||
document.getElementById('_fb-title').value = '';
|
document.getElementById('_fb-title').value = '';
|
||||||
document.getElementById('_fb-desc').value = '';
|
document.getElementById('_fb-desc').value = '';
|
||||||
|
document.getElementById('_fb-assignee').value = '';
|
||||||
document.getElementById('_fb-snippet-wrap').style.display = 'none';
|
document.getElementById('_fb-snippet-wrap').style.display = 'none';
|
||||||
document.getElementById('_fb-pick-hint').textContent = 'Click to reference a page element';
|
document.getElementById('_fb-pick-hint').textContent = 'Click to reference a page element';
|
||||||
capturedSnippet = '';
|
capturedSnippet = '';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user