feat: HTMX-ify article publish/delete actions for inline updates
- Publish/Unpublish returns updated <tr> partial via HTMX - Delete returns empty string to remove row without page reload - Extract article_row.html partial (used by both results table and individual HTMX responses) - article_results.html now includes article_row.html via loop Subtask 7 of CMS admin improvement. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1849,6 +1849,9 @@ async def article_delete(article_id: int):
|
||||
from ..sitemap import invalidate_sitemap_cache
|
||||
invalidate_sitemap_cache()
|
||||
|
||||
if request.headers.get("HX-Request"):
|
||||
return "" # row removed via hx-swap="outerHTML"
|
||||
|
||||
await flash("Article deleted.", "success")
|
||||
return redirect(url_for("admin.articles"))
|
||||
|
||||
@@ -1873,6 +1876,18 @@ async def article_publish(article_id: int):
|
||||
from ..sitemap import invalidate_sitemap_cache
|
||||
invalidate_sitemap_cache()
|
||||
|
||||
if request.headers.get("HX-Request"):
|
||||
updated = await fetch_one(
|
||||
"""SELECT *,
|
||||
CASE WHEN status = 'published' AND published_at > datetime('now')
|
||||
THEN 'scheduled'
|
||||
WHEN status = 'published' THEN 'live'
|
||||
ELSE status END AS display_status
|
||||
FROM articles WHERE id = ?""",
|
||||
(article_id,),
|
||||
)
|
||||
return await render_template("admin/partials/article_row.html", a=updated)
|
||||
|
||||
await flash(f"Article status changed to {new_status}.", "success")
|
||||
return redirect(url_for("admin.articles"))
|
||||
|
||||
|
||||
@@ -13,38 +13,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in articles %}
|
||||
<tr>
|
||||
<td style="max-width:280px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap"
|
||||
title="{{ a.url_path }}">{{ a.title }}</td>
|
||||
<td>
|
||||
{% if a.display_status == 'live' %}
|
||||
<span class="badge-success">Live</span>
|
||||
{% elif a.display_status == 'scheduled' %}
|
||||
<span class="badge-warning">Scheduled</span>
|
||||
{% else %}
|
||||
<span class="badge">Draft</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="mono">{{ a.published_at[:10] if a.published_at else '-' }}</td>
|
||||
<td>{{ a.language | upper if a.language else '-' }}</td>
|
||||
<td class="text-slate">{{ a.template_slug or 'Manual' }}</td>
|
||||
<td class="text-right" style="white-space:nowrap">
|
||||
{% if a.display_status == 'live' %}
|
||||
<a href="/{{ a.language or 'en' }}{{ a.url_path }}" target="_blank" class="btn-outline btn-sm">View</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('admin.article_edit', article_id=a.id) }}" class="btn-outline btn-sm">Edit</a>
|
||||
<form method="post" action="{{ url_for('admin.article_publish', article_id=a.id) }}" class="m-0" style="display:inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn-outline btn-sm">
|
||||
{% if a.display_status != 'draft' %}Unpublish{% else %}Publish{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('admin.article_delete', article_id=a.id) }}" class="m-0" style="display:inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn-outline btn-sm" onclick="return confirm('Delete?')">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% include "admin/partials/article_row.html" %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<tr id="article-{{ a.id }}">
|
||||
<td style="max-width:280px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap"
|
||||
title="{{ a.url_path }}">{{ a.title }}</td>
|
||||
<td>
|
||||
{% if a.display_status == 'live' %}
|
||||
<span class="badge-success">Live</span>
|
||||
{% elif a.display_status == 'scheduled' %}
|
||||
<span class="badge-warning">Scheduled</span>
|
||||
{% else %}
|
||||
<span class="badge">Draft</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="mono">{{ a.published_at[:10] if a.published_at else '-' }}</td>
|
||||
<td>{{ a.language | upper if a.language else '-' }}</td>
|
||||
<td class="text-slate">{{ a.template_slug or 'Manual' }}</td>
|
||||
<td class="text-right" style="white-space:nowrap">
|
||||
{% if a.display_status == 'live' %}
|
||||
<a href="/{{ a.language or 'en' }}{{ a.url_path }}" target="_blank" class="btn-outline btn-sm">View</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('admin.article_edit', article_id=a.id) }}" class="btn-outline btn-sm">Edit</a>
|
||||
<button hx-post="{{ url_for('admin.article_publish', article_id=a.id) }}"
|
||||
hx-target="#article-{{ a.id }}" hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRF-Token": "{{ csrf_token() }}"}'
|
||||
class="btn-outline btn-sm">
|
||||
{% if a.display_status != 'draft' %}Unpublish{% else %}Publish{% endif %}
|
||||
</button>
|
||||
<button hx-post="{{ url_for('admin.article_delete', article_id=a.id) }}"
|
||||
hx-target="#article-{{ a.id }}" hx-swap="outerHTML swap:200ms"
|
||||
hx-confirm="Delete this article?"
|
||||
hx-headers='{"X-CSRF-Token": "{{ csrf_token() }}"}'
|
||||
class="btn-outline btn-sm">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
Reference in New Issue
Block a user