Admin: inline flag-reason popover (replaces prompt())
Per Codex's polish note: flagging a source now opens a small inline popover for the optional reason (consistent with the calm admin UI) instead of a native prompt(). Clearing a flag stays immediate. Backdrop/Escape/Cancel close it; Enter confirms. Optimistic with revert-on-failure, like the other actions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,18 +84,24 @@
|
||||
try { await postJSON(`/api/admin/sources/${s.id}/active`, { active: next }); }
|
||||
catch { s.active = next ? 0 : 1; }
|
||||
}
|
||||
async function toggleReview(s) {
|
||||
if (s.review_flag) {
|
||||
const prevR = s.review_reason;
|
||||
s.review_flag = 0; s.review_reason = null;
|
||||
try { await postJSON(`/api/admin/sources/${s.id}/review`, { flag: false }); }
|
||||
catch { s.review_flag = 1; s.review_reason = prevR; }
|
||||
} else {
|
||||
const reason = (prompt('Flag this source for review — optional note:') || '').trim();
|
||||
s.review_flag = 1; s.review_reason = reason || null;
|
||||
try { await postJSON(`/api/admin/sources/${s.id}/review`, { flag: true, reason: reason || null }); }
|
||||
catch { s.review_flag = 0; s.review_reason = null; }
|
||||
}
|
||||
// Flagging opens a small inline popover for the reason; clearing is immediate.
|
||||
let flagging = $state(null); // the source being flagged
|
||||
let flagReason = $state('');
|
||||
function openFlag(s) { flagging = s; flagReason = ''; }
|
||||
function cancelFlag() { flagging = null; flagReason = ''; }
|
||||
async function confirmFlag() {
|
||||
const s = flagging;
|
||||
const reason = flagReason.trim() || null;
|
||||
s.review_flag = 1; s.review_reason = reason; // optimistic
|
||||
flagging = null; flagReason = '';
|
||||
try { await postJSON(`/api/admin/sources/${s.id}/review`, { flag: true, reason }); }
|
||||
catch { s.review_flag = 0; s.review_reason = null; }
|
||||
}
|
||||
async function clearReview(s) {
|
||||
const prevR = s.review_reason;
|
||||
s.review_flag = 0; s.review_reason = null; // optimistic
|
||||
try { await postJSON(`/api/admin/sources/${s.id}/review`, { flag: false }); }
|
||||
catch { s.review_flag = 1; s.review_reason = prevR; }
|
||||
}
|
||||
|
||||
// Feedback inbox: filter + read/unread + delete.
|
||||
@@ -322,7 +328,7 @@
|
||||
</td>
|
||||
<td class="rowactions">
|
||||
<button class="act" onclick={() => toggleActive(s)}>{s.active ? 'Pause' : 'Resume'}</button>
|
||||
<button class="act" onclick={() => toggleReview(s)}>{s.review_flag ? 'Clear' : 'Flag'}</button>
|
||||
<button class="act" onclick={() => (s.review_flag ? clearReview(s) : openFlag(s))}>{s.review_flag ? 'Clear' : 'Flag'}</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
@@ -465,6 +471,27 @@
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
{#if flagging}
|
||||
<!-- Flag-reason popover: backdrop closes; Escape/Cancel too (keyboard path). -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="modal-overlay" role="presentation" onclick={(e) => e.target === e.currentTarget && cancelFlag()}>
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-label="Flag source for review" tabindex="-1">
|
||||
<h3>Flag “{flagging.name}” for review</h3>
|
||||
<p class="msub">An optional note on why — it shows under the source in the table.</p>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={flagReason}
|
||||
placeholder="e.g. acceptance dropping, off-topic lately"
|
||||
onkeydown={(e) => (e.key === 'Enter' ? confirmFlag() : e.key === 'Escape' ? cancelFlag() : null)}
|
||||
/>
|
||||
<div class="mbtns">
|
||||
<button class="msend" onclick={confirmFlag}>Flag for review</button>
|
||||
<button class="act" onclick={cancelFlag}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
header.bar { background: var(--surface); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 20; }
|
||||
.inner { display: flex; align-items: center; justify-content: space-between; height: 64px; }
|
||||
@@ -614,6 +641,29 @@
|
||||
.csend:hover { background: var(--accent-deep); }
|
||||
.csend:disabled { opacity: 0.55; cursor: default; }
|
||||
.noreply { color: var(--muted); font-size: 0.76rem; font-style: italic; }
|
||||
|
||||
/* Flag-reason popover */
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0; background: rgba(10, 22, 38, 0.32);
|
||||
display: flex; align-items: center; justify-content: center; padding: 20px; z-index: 60;
|
||||
}
|
||||
.modal {
|
||||
background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
|
||||
box-shadow: 0 12px 34px rgba(40, 38, 28, 0.16); width: 100%; max-width: 420px; padding: 22px 24px;
|
||||
}
|
||||
.modal h3 { margin: 0 0 6px; font-size: 1.2rem; }
|
||||
.modal .msub { margin: 0 0 14px; color: var(--muted); font-size: 0.85rem; }
|
||||
.modal input {
|
||||
width: 100%; box-sizing: border-box; font: inherit; font-size: 0.92rem;
|
||||
padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--bg); color: var(--ink);
|
||||
}
|
||||
.modal input:focus { outline: none; border-color: var(--accent); }
|
||||
.mbtns { display: flex; align-items: center; gap: 12px; margin-top: 14px; }
|
||||
.msend {
|
||||
font: inherit; font-size: 0.85rem; font-weight: 600; background: var(--accent); color: #fff;
|
||||
border: none; border-radius: 999px; padding: 8px 18px; cursor: pointer;
|
||||
}
|
||||
.msend:hover { background: var(--accent-deep); }
|
||||
.factions { display: flex; gap: 14px; margin-top: 9px; }
|
||||
.factions .act {
|
||||
background: none; border: none; padding: 0; cursor: pointer;
|
||||
|
||||
Reference in New Issue
Block a user