Ideas: add 'won't implement' status with decline/reopen button
This commit is contained in:
@@ -45,7 +45,7 @@ async def list_ideas(
|
|||||||
ideas.append(idea)
|
ideas.append(idea)
|
||||||
def _sort_key(x: dict):
|
def _sort_key(x: dict):
|
||||||
s = x.get("status") or "open"
|
s = x.get("status") or "open"
|
||||||
order = {"awaiting": 0, "open": 1, "done": 2}
|
order = {"awaiting": 0, "open": 1, "done": 2, "declined": 3}
|
||||||
return (order.get(s, 1), -x["vote_count"], -x["created_at"])
|
return (order.get(s, 1), -x["vote_count"], -x["created_at"])
|
||||||
ideas.sort(key=_sort_key)
|
ideas.sort(key=_sort_key)
|
||||||
return JSONResponse({"ideas": ideas})
|
return JSONResponse({"ideas": ideas})
|
||||||
@@ -126,6 +126,28 @@ async def toggle_idea_status(
|
|||||||
return JSONResponse({"status": idea["status"]})
|
return JSONResponse({"status": idea["status"]})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/ideas/{idea_id}/decline")
|
||||||
|
async def decline_idea(
|
||||||
|
idea_id: str,
|
||||||
|
bincio_session: Optional[str] = Cookie(default=None),
|
||||||
|
) -> JSONResponse:
|
||||||
|
user = deps._require_user(bincio_session)
|
||||||
|
if not user.is_admin:
|
||||||
|
raise HTTPException(403, "Forbidden")
|
||||||
|
dd = deps._get_data_dir()
|
||||||
|
path = _ideas_dir(dd) / f"{idea_id}.json"
|
||||||
|
if not path.exists():
|
||||||
|
raise HTTPException(404, "Not found")
|
||||||
|
with open(path, "r+", encoding="utf-8") as f:
|
||||||
|
_fcntl.flock(f, _fcntl.LOCK_EX)
|
||||||
|
idea = json.load(f)
|
||||||
|
idea["status"] = "open" if idea.get("status") == "declined" else "declined"
|
||||||
|
f.seek(0)
|
||||||
|
f.truncate()
|
||||||
|
json.dump(idea, f, ensure_ascii=False, indent=2)
|
||||||
|
return JSONResponse({"status": idea["status"]})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/ideas/{idea_id}/comment")
|
@router.post("/api/ideas/{idea_id}/comment")
|
||||||
async def set_idea_comment(
|
async def set_idea_comment(
|
||||||
idea_id: str,
|
idea_id: str,
|
||||||
|
|||||||
@@ -181,7 +181,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusOrder(s: string | undefined) {
|
function statusOrder(s: string | undefined) {
|
||||||
return s === 'awaiting' ? 0 : s === 'done' ? 2 : 1;
|
return s === 'awaiting' ? 0 : s === 'done' ? 2 : s === 'declined' ? 3 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function declineIdea(idea: Idea) {
|
||||||
|
const r = await fetch(`/api/ideas/${idea.id}/decline`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
const d = await r.json();
|
||||||
|
idea.status = d.status;
|
||||||
|
ideas = [...ideas].sort((a, b) =>
|
||||||
|
statusOrder(a.status) - statusOrder(b.status) ||
|
||||||
|
b.vote_count - a.vote_count ||
|
||||||
|
b.created_at - a.created_at
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleStatus(idea: Idea) {
|
async function toggleStatus(idea: Idea) {
|
||||||
@@ -201,7 +217,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: awaitingIdeas = ideas.filter(i => i.status === 'awaiting');
|
$: awaitingIdeas = ideas.filter(i => i.status === 'awaiting');
|
||||||
$: otherIdeas = ideas.filter(i => i.status !== 'awaiting');
|
$: otherIdeas = ideas.filter(i => i.status !== 'awaiting' && i.status !== 'declined');
|
||||||
|
$: declinedIdeas = ideas.filter(i => i.status === 'declined');
|
||||||
|
|
||||||
async function deleteIdea(idea: Idea) {
|
async function deleteIdea(idea: Idea) {
|
||||||
if (!confirm(`Delete "${idea.title}"?`)) return;
|
if (!confirm(`Delete "${idea.title}"?`)) return;
|
||||||
@@ -301,11 +318,30 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if declinedIdeas.length > 0}
|
||||||
|
<div class="mt-6">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: #71717a">
|
||||||
|
✗ Won't implement
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
{#each declinedIdeas as idea (idea.id)}
|
||||||
|
<li
|
||||||
|
class="rounded-xl border px-4 py-3 flex gap-3"
|
||||||
|
style="background: var(--bg-card); border-color: var(--border); opacity: 0.4"
|
||||||
|
>
|
||||||
|
{@render ideaContent(idea)}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#snippet ideaContent(idea: Idea)}
|
{#snippet ideaContent(idea: Idea)}
|
||||||
{@const done = idea.status === 'done'}
|
{@const done = idea.status === 'done'}
|
||||||
{@const awaiting = idea.status === 'awaiting'}
|
{@const awaiting = idea.status === 'awaiting'}
|
||||||
|
{@const declined = idea.status === 'declined'}
|
||||||
<!-- Vote button -->
|
<!-- Vote button -->
|
||||||
<div class="flex flex-col items-center shrink-0 pt-0.5">
|
<div class="flex flex-col items-center shrink-0 pt-0.5">
|
||||||
<button
|
<button
|
||||||
@@ -351,10 +387,18 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<p class="font-medium text-sm" style="color: var(--text-primary)">
|
<p class="font-medium text-sm" style="color: var(--text-primary)">
|
||||||
{#if done}<span class="mr-1.5 text-green-500">✓</span>{/if}{idea.title}
|
{#if done}<span class="mr-1.5 text-green-500">✓</span>{:else if declined}<span class="mr-1.5" style="color:#71717a">✗</span>{/if}{idea.title}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
|
{#if declined}
|
||||||
|
<button
|
||||||
|
on:click={() => declineIdea(idea)}
|
||||||
|
class="text-xs px-1.5 py-0.5 rounded transition-colors"
|
||||||
|
style="color: var(--text-5); border: 1px solid var(--border-sub)"
|
||||||
|
title="Reopen"
|
||||||
|
>↩</button>
|
||||||
|
{:else}
|
||||||
{@const nextLabel = done ? 'reopen' : awaiting ? 'done' : 'awaiting'}
|
{@const nextLabel = done ? 'reopen' : awaiting ? 'done' : 'awaiting'}
|
||||||
{@const btnColor = done ? 'var(--text-5)' : awaiting ? 'var(--accent)' : '#f59e0b'}
|
{@const btnColor = done ? 'var(--text-5)' : awaiting ? 'var(--accent)' : '#f59e0b'}
|
||||||
{@const borderColor = done ? 'var(--border-sub)' : awaiting ? 'var(--accent)' : '#f59e0b'}
|
{@const borderColor = done ? 'var(--border-sub)' : awaiting ? 'var(--accent)' : '#f59e0b'}
|
||||||
@@ -364,6 +408,13 @@
|
|||||||
style="color: {btnColor}; border: 1px solid {borderColor}"
|
style="color: {btnColor}; border: 1px solid {borderColor}"
|
||||||
title="Cycle status"
|
title="Cycle status"
|
||||||
>{nextLabel}</button>
|
>{nextLabel}</button>
|
||||||
|
<button
|
||||||
|
on:click={() => declineIdea(idea)}
|
||||||
|
class="text-xs px-1.5 py-0.5 rounded transition-colors"
|
||||||
|
style="color: #ef4444; border: 1px solid rgba(239,68,68,0.35)"
|
||||||
|
title="Won't implement"
|
||||||
|
>✗</button>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if isAdmin || idea.author === myHandle}
|
{#if isAdmin || idea.author === myHandle}
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user