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 }); }
|
try { await postJSON(`/api/admin/sources/${s.id}/active`, { active: next }); }
|
||||||
catch { s.active = next ? 0 : 1; }
|
catch { s.active = next ? 0 : 1; }
|
||||||
}
|
}
|
||||||
async function toggleReview(s) {
|
// Flagging opens a small inline popover for the reason; clearing is immediate.
|
||||||
if (s.review_flag) {
|
let flagging = $state(null); // the source being flagged
|
||||||
const prevR = s.review_reason;
|
let flagReason = $state('');
|
||||||
s.review_flag = 0; s.review_reason = null;
|
function openFlag(s) { flagging = s; flagReason = ''; }
|
||||||
try { await postJSON(`/api/admin/sources/${s.id}/review`, { flag: false }); }
|
function cancelFlag() { flagging = null; flagReason = ''; }
|
||||||
catch { s.review_flag = 1; s.review_reason = prevR; }
|
async function confirmFlag() {
|
||||||
} else {
|
const s = flagging;
|
||||||
const reason = (prompt('Flag this source for review — optional note:') || '').trim();
|
const reason = flagReason.trim() || null;
|
||||||
s.review_flag = 1; s.review_reason = reason || null;
|
s.review_flag = 1; s.review_reason = reason; // optimistic
|
||||||
try { await postJSON(`/api/admin/sources/${s.id}/review`, { flag: true, reason: reason || null }); }
|
flagging = null; flagReason = '';
|
||||||
|
try { await postJSON(`/api/admin/sources/${s.id}/review`, { flag: true, reason }); }
|
||||||
catch { s.review_flag = 0; s.review_reason = null; }
|
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.
|
// Feedback inbox: filter + read/unread + delete.
|
||||||
@@ -322,7 +328,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="rowactions">
|
<td class="rowactions">
|
||||||
<button class="act" onclick={() => toggleActive(s)}>{s.active ? 'Pause' : 'Resume'}</button>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -465,6 +471,27 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</main>
|
</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>
|
<style>
|
||||||
header.bar { background: var(--surface); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 20; }
|
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; }
|
.inner { display: flex; align-items: center; justify-content: space-between; height: 64px; }
|
||||||
@@ -614,6 +641,29 @@
|
|||||||
.csend:hover { background: var(--accent-deep); }
|
.csend:hover { background: var(--accent-deep); }
|
||||||
.csend:disabled { opacity: 0.55; cursor: default; }
|
.csend:disabled { opacity: 0.55; cursor: default; }
|
||||||
.noreply { color: var(--muted); font-size: 0.76rem; font-style: italic; }
|
.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 { display: flex; gap: 14px; margin-top: 9px; }
|
||||||
.factions .act {
|
.factions .act {
|
||||||
background: none; border: none; padding: 0; cursor: pointer;
|
background: none; border: none; padding: 0; cursor: pointer;
|
||||||
|
|||||||
Reference in New Issue
Block a user