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