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:
Deeman
2026-02-24 01:20:02 +01:00
parent 8ae5aa1935
commit a59c670e43
3 changed files with 49 additions and 32 deletions

View File

@@ -1849,6 +1849,9 @@ async def article_delete(article_id: int):
from ..sitemap import invalidate_sitemap_cache from ..sitemap import invalidate_sitemap_cache
invalidate_sitemap_cache() invalidate_sitemap_cache()
if request.headers.get("HX-Request"):
return "" # row removed via hx-swap="outerHTML"
await flash("Article deleted.", "success") await flash("Article deleted.", "success")
return redirect(url_for("admin.articles")) return redirect(url_for("admin.articles"))
@@ -1873,6 +1876,18 @@ async def article_publish(article_id: int):
from ..sitemap import invalidate_sitemap_cache from ..sitemap import invalidate_sitemap_cache
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") await flash(f"Article status changed to {new_status}.", "success")
return redirect(url_for("admin.articles")) return redirect(url_for("admin.articles"))

View File

@@ -13,38 +13,7 @@
</thead> </thead>
<tbody> <tbody>
{% for a in articles %} {% for a in articles %}
<tr> {% include "admin/partials/article_row.html" %}
<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>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -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>