diff --git a/README.md b/README.md index df98c4e..fa4a926 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,12 @@ python3 -m pip install -r requirements.txt Point the dashboard at the Gitea server root (without `/api/v1`) and provide a token that can read dashboard data, update the authenticated user's notification -threads, create issue comments, close assigned issues, and submit pull-request -reviews. Pull-request replies and mobile My Work issue comments use Gitea's -issue-comment API; closing an assigned issue and native Comment, Approve, and -Request changes reviews require repository write permission. Serve the dashboard +threads, create and self-assign issues, create issue comments, close assigned +issues, and submit pull-request reviews. Pull-request replies and mobile My Work +issue comments use Gitea's issue-comment API; mobile issue capture requires issue +creation and assignment permission, while closing an assigned issue and native +Comment, Approve, and Request changes reviews require repository write permission. +Serve the dashboard only to trusted users on its own origin; cross-origin API access is intentionally disabled. Then start the API and bundled frontend: diff --git a/frontend/create-issue-sheet.js b/frontend/create-issue-sheet.js new file mode 100644 index 0000000..0b5b18e --- /dev/null +++ b/frontend/create-issue-sheet.js @@ -0,0 +1,53 @@ +function createIssueCapture({ fetchJson, storage }) { + const storageKey = 'stackchain.issue-capture.v1'; + let pending = null; + const emptyDraft = () => ({ repository: '', title: '', body: '' }); + + function saveDraft(draft) { + const safe = { + repository: String(draft?.repository || ''), + title: String(draft?.title || ''), + body: String(draft?.body || ''), + }; + try { storage.setItem(storageKey, JSON.stringify(safe)); } + catch (_error) { /* Keep the form as the in-memory fallback. */ } + return safe; + } + + function loadDraft() { + try { + const parsed = JSON.parse(storage.getItem(storageKey) || 'null'); + return parsed && typeof parsed === 'object' ? { + repository: String(parsed.repository || ''), + title: String(parsed.title || ''), + body: String(parsed.body || ''), + } : emptyDraft(); + } catch (_error) { + return emptyDraft(); + } + } + + function clearDraft() { + try { storage.removeItem(storageKey); } + catch (_error) { /* Confirmed creation remains authoritative. */ } + } + + function submit(draft) { + if (pending) return pending; + const saved = saveDraft(draft); + const repository = saved.repository.split('/').map(encodeURIComponent).join('/'); + pending = fetchJson('api/v1/repos/' + repository + '/issues', { + method: 'POST', + headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: saved.title, body: saved.body }), + }).then(issue => { + clearDraft(); + return issue; + }).finally(() => { pending = null; }); + return pending; + } + + return { saveDraft, loadDraft, submit }; +} + +if (typeof module !== 'undefined' && module.exports) module.exports = createIssueCapture; diff --git a/frontend/index.html b/frontend/index.html index c3ab279..a686200 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -129,6 +129,16 @@ textarea { resize: vertical; min-height: 120px; } .issue-sheet-actions button, .issue-sheet-actions a { min-height:44px; display:flex; align-items:center; justify-content:center; } .issue-sheet-actions a { border:1px solid #60a5fa; border-radius:10px; font-weight:700; } .issue-retry { min-height:44px; width:100%; margin-top:10px; } +.new-issue { min-height:44px; } +.create-issue-sheet { position:fixed; inset:0; z-index:57; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); } +.create-issue-sheet.open { display:flex; } +.create-issue-panel { width:min(560px,100%); height:100dvh; overflow:auto; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; } +.create-issue-header { display:flex; align-items:center; justify-content:space-between; gap:10px; } +.create-issue-header button, .create-issue-actions button { min-height:44px; } +.create-issue-form { display:grid; gap:12px; } +.create-issue-form label { display:grid; gap:6px; } +.create-issue-form select { min-height:44px; padding:8px; border-radius:8px; border:1px solid #1f3a5f; background:#0b1526; color:#e5e7eb; } +.create-issue-actions { position:sticky; bottom:0; display:grid; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; } @media (max-width: 600px) { header { align-items:flex-start; } .my-work { margin:0; } @@ -138,6 +148,7 @@ textarea { resize: vertical; min-height: 120px; } .review-sheet-panel { width:100%; border-left:0; padding:14px; } .update-sheet-panel { width:100%; border-left:0; padding:14px; } .issue-sheet-panel { width:100%; border-left:0; padding:14px; } + .create-issue-panel { width:100%; border-left:0; padding:14px; } } .obi { width:14px; height:14px; background: url('data:image/svg+xml;utf8,') center/contain no-repeat; display:inline-block; } .footer { padding: 12px; text-align: center; color:#4e6b8a; font-size:12px; } @@ -163,6 +174,7 @@ textarea { resize: vertical; min-height: 120px; }