Candidates: inline rename (fix a name typo without reject + re-add)

A staged candidate could only be renamed by rejecting and re-adding it, which
churns the queue and discards the preview just to fix a typo. Add an inline
Rename on each candidate: a "Rename" pill swaps the name for an input
(Enter saves · Esc cancels), POST /api/admin/candidates/{id}/rename →
sources.rename_candidate(). Empty clears the name (promote then derives one
from the feed host). Preview is preserved; the fixed name carries into promotion.

Tests: test_candidate_rename (rename in place keeps preview, promotes with the
new name, gated + 404). 225 pytest + 11 vitest green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-11 21:39:13 -04:00
parent 3afc1ed37e
commit 070b40584e
4 changed files with 68 additions and 3 deletions
+23 -3
View File
@@ -293,6 +293,12 @@
Object.assign(c, res, { _deep: false });
} catch (e) { c._err = e?.message || 'Deep preview failed.'; c._deep = false; }
}
function startRename(c) { c._editName = c.name || ''; c._editing = true; c._err = ''; }
async function saveRename(c) {
try {
Object.assign(c, await postJSON(`/api/admin/candidates/${c.id}/rename`, { name: (c._editName || '').trim() }), { _editing: false });
} catch (e) { c._err = e?.message || 'Rename failed.'; }
}
async function promoteCandidate(c) {
c._err = '';
try {
@@ -570,8 +576,16 @@
{#each pendingCandidates as c (c.id)}
<li>
<div class="chead">
<span class="cname">{c.name || c.feed_url}</span>
<span class="cstatus">{c.status}</span>
{#if c._editing}
<input class="crename" bind:value={c._editName} placeholder="Source name"
onkeydown={(e) => (e.key === 'Enter' ? saveRename(c) : e.key === 'Escape' ? (c._editing = false) : null)} />
<button class="act" onclick={() => saveRename(c)}>Save</button>
<button class="act" onclick={() => (c._editing = false)}>Cancel</button>
{:else}
<span class="cname">{c.name || c.feed_url}</span>
<button class="act mini" onclick={() => startRename(c)}>Rename</button>
<span class="cstatus">{c.status}</span>
{/if}
</div>
<div class="curl">{c.feed_url}</div>
{#if c.preview}
@@ -1102,9 +1116,15 @@
.addrow input:focus { outline: none; border-color: var(--accent); }
ul.candlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
ul.candlist li { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; }
.chead { display: flex; align-items: baseline; gap: 10px; }
.chead { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.chead .cname { font-weight: 600; color: var(--ink); }
.chead .cstatus { font-size: 0.72rem; color: var(--muted); text-transform: capitalize; }
.chead .crename { font: inherit; font-size: 0.9rem; font-weight: 600; padding: 4px 9px;
border: 1px solid var(--accent); border-radius: 8px; background: var(--bg); color: var(--ink); }
.chead .crename:focus { outline: none; }
.act.mini { font-size: 0.72rem; padding: 2px 8px; color: var(--accent-deep);
background: none; border: 1px solid var(--line); border-radius: 999px; cursor: pointer; }
.act.mini:hover { border-color: var(--accent); }
.curl { font-size: 0.76rem; color: var(--muted); word-break: break-all; margin-top: 2px; }
.cprev { font-size: 0.84rem; color: var(--ink); margin-top: 7px; }
.cprev .cex { color: var(--muted); font-size: 0.8rem; margin-top: 2px; font-style: italic; }