Compare commits
2 Commits
v202603062
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d86669360 | ||
|
|
7d523250f7 |
@@ -2275,15 +2275,17 @@ async def _sync_static_articles() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _get_article_list(
|
def _build_article_where(
|
||||||
status: str = None,
|
status: str = None,
|
||||||
template_slug: str = None,
|
template_slug: str = None,
|
||||||
language: str = None,
|
language: str = None,
|
||||||
search: str = None,
|
search: str = None,
|
||||||
page: int = 1,
|
) -> tuple[list[str], list]:
|
||||||
per_page: int = 50,
|
"""Build WHERE clauses and params for article queries.
|
||||||
) -> list[dict]:
|
|
||||||
"""Get articles with optional filters and pagination."""
|
template_slug='__manual__' filters for articles with template_slug IS NULL
|
||||||
|
(cornerstone / manually written articles, no pSEO template).
|
||||||
|
"""
|
||||||
wheres = ["1=1"]
|
wheres = ["1=1"]
|
||||||
params: list = []
|
params: list = []
|
||||||
|
|
||||||
@@ -2293,7 +2295,9 @@ async def _get_article_list(
|
|||||||
wheres.append("status = 'published' AND published_at > datetime('now')")
|
wheres.append("status = 'published' AND published_at > datetime('now')")
|
||||||
elif status == "draft":
|
elif status == "draft":
|
||||||
wheres.append("status = 'draft'")
|
wheres.append("status = 'draft'")
|
||||||
if template_slug:
|
if template_slug == "__manual__":
|
||||||
|
wheres.append("template_slug IS NULL")
|
||||||
|
elif template_slug:
|
||||||
wheres.append("template_slug = ?")
|
wheres.append("template_slug = ?")
|
||||||
params.append(template_slug)
|
params.append(template_slug)
|
||||||
if language:
|
if language:
|
||||||
@@ -2303,6 +2307,20 @@ async def _get_article_list(
|
|||||||
wheres.append("title LIKE ?")
|
wheres.append("title LIKE ?")
|
||||||
params.append(f"%{search}%")
|
params.append(f"%{search}%")
|
||||||
|
|
||||||
|
return wheres, params
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_article_list(
|
||||||
|
status: str = None,
|
||||||
|
template_slug: str = None,
|
||||||
|
language: str = None,
|
||||||
|
search: str = None,
|
||||||
|
page: int = 1,
|
||||||
|
per_page: int = 50,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Get articles with optional filters and pagination."""
|
||||||
|
wheres, params = _build_article_where(status=status, template_slug=template_slug,
|
||||||
|
language=language, search=search)
|
||||||
where = " AND ".join(wheres)
|
where = " AND ".join(wheres)
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
params.extend([per_page, offset])
|
params.extend([per_page, offset])
|
||||||
@@ -2332,22 +2350,8 @@ async def _get_article_list_grouped(
|
|||||||
Static cornerstones (group_key e.g. 'C2') group by cornerstone key regardless of url_path.
|
Static cornerstones (group_key e.g. 'C2') group by cornerstone key regardless of url_path.
|
||||||
Each returned item has a 'variants' list (one dict per language variant).
|
Each returned item has a 'variants' list (one dict per language variant).
|
||||||
"""
|
"""
|
||||||
wheres = ["1=1"]
|
wheres, params = _build_article_where(status=status, template_slug=template_slug,
|
||||||
params: list = []
|
search=search)
|
||||||
|
|
||||||
if status == "live":
|
|
||||||
wheres.append("status = 'published' AND published_at <= datetime('now')")
|
|
||||||
elif status == "scheduled":
|
|
||||||
wheres.append("status = 'published' AND published_at > datetime('now')")
|
|
||||||
elif status == "draft":
|
|
||||||
wheres.append("status = 'draft'")
|
|
||||||
if template_slug:
|
|
||||||
wheres.append("template_slug = ?")
|
|
||||||
params.append(template_slug)
|
|
||||||
if search:
|
|
||||||
wheres.append("title LIKE ?")
|
|
||||||
params.append(f"%{search}%")
|
|
||||||
|
|
||||||
where = " AND ".join(wheres)
|
where = " AND ".join(wheres)
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
|
|
||||||
@@ -2507,26 +2511,131 @@ async def article_results():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/articles/matching-count")
|
||||||
|
@role_required("admin")
|
||||||
|
async def articles_matching_count():
|
||||||
|
"""Return count of articles matching current filters (for bulk select-all banner)."""
|
||||||
|
status_filter = request.args.get("status", "")
|
||||||
|
template_filter = request.args.get("template", "")
|
||||||
|
language_filter = request.args.get("language", "")
|
||||||
|
search = request.args.get("search", "").strip()
|
||||||
|
|
||||||
|
wheres, params = _build_article_where(
|
||||||
|
status=status_filter or None,
|
||||||
|
template_slug=template_filter or None,
|
||||||
|
language=language_filter or None,
|
||||||
|
search=search or None,
|
||||||
|
)
|
||||||
|
where = " AND ".join(wheres)
|
||||||
|
row = await fetch_one(f"SELECT COUNT(*) AS cnt FROM articles WHERE {where}", tuple(params))
|
||||||
|
count = row["cnt"] if row else 0
|
||||||
|
return f"{count:,}"
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/articles/bulk", methods=["POST"])
|
@bp.route("/articles/bulk", methods=["POST"])
|
||||||
@role_required("admin")
|
@role_required("admin")
|
||||||
@csrf_protect
|
@csrf_protect
|
||||||
async def articles_bulk():
|
async def articles_bulk():
|
||||||
"""Bulk actions on articles: publish, unpublish, toggle_noindex, rebuild, delete."""
|
"""Bulk actions on articles: publish, unpublish, toggle_noindex, rebuild, delete.
|
||||||
|
|
||||||
|
Supports two modes:
|
||||||
|
- Explicit IDs: article_ids=1,2,3 (max 500)
|
||||||
|
- Apply to all matching: apply_to_all=true + filter params (rebuild capped at 2000, delete at 5000)
|
||||||
|
"""
|
||||||
form = await request.form
|
form = await request.form
|
||||||
ids_raw = form.get("article_ids", "").strip()
|
|
||||||
action = form.get("action", "").strip()
|
action = form.get("action", "").strip()
|
||||||
|
apply_to_all = form.get("apply_to_all", "").strip() == "true"
|
||||||
|
|
||||||
|
# Common filter params (used for action scope and re-render)
|
||||||
|
search = form.get("search", "").strip()
|
||||||
|
status_filter = form.get("status", "")
|
||||||
|
template_filter = form.get("template", "")
|
||||||
|
language_filter = form.get("language", "")
|
||||||
|
|
||||||
valid_actions = ("publish", "unpublish", "toggle_noindex", "rebuild", "delete")
|
valid_actions = ("publish", "unpublish", "toggle_noindex", "rebuild", "delete")
|
||||||
if action not in valid_actions or not ids_raw:
|
if action not in valid_actions:
|
||||||
return "", 400
|
return "", 400
|
||||||
|
|
||||||
|
now = utcnow_iso()
|
||||||
|
|
||||||
|
if apply_to_all:
|
||||||
|
wheres, where_params = _build_article_where(
|
||||||
|
status=status_filter or None,
|
||||||
|
template_slug=template_filter or None,
|
||||||
|
language=language_filter or None,
|
||||||
|
search=search or None,
|
||||||
|
)
|
||||||
|
where = " AND ".join(wheres)
|
||||||
|
|
||||||
|
if action == "rebuild":
|
||||||
|
count_row = await fetch_one(
|
||||||
|
f"SELECT COUNT(*) AS cnt FROM articles WHERE {where}", tuple(where_params)
|
||||||
|
)
|
||||||
|
count = count_row["cnt"] if count_row else 0
|
||||||
|
if count > 2000:
|
||||||
|
return (
|
||||||
|
f"<p class='text-red-600 p-4'>Too many articles ({count:,}) for bulk rebuild"
|
||||||
|
f" — max 2,000. Narrow your filters first.</p>",
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "publish":
|
||||||
|
await execute(
|
||||||
|
f"UPDATE articles SET status = 'published', updated_at = ? WHERE {where}",
|
||||||
|
(now, *where_params),
|
||||||
|
)
|
||||||
|
from ..sitemap import invalidate_sitemap_cache
|
||||||
|
invalidate_sitemap_cache()
|
||||||
|
|
||||||
|
elif action == "unpublish":
|
||||||
|
await execute(
|
||||||
|
f"UPDATE articles SET status = 'draft', updated_at = ? WHERE {where}",
|
||||||
|
(now, *where_params),
|
||||||
|
)
|
||||||
|
from ..sitemap import invalidate_sitemap_cache
|
||||||
|
invalidate_sitemap_cache()
|
||||||
|
|
||||||
|
elif action == "toggle_noindex":
|
||||||
|
await execute(
|
||||||
|
f"UPDATE articles SET noindex = CASE WHEN noindex = 1 THEN 0 ELSE 1 END,"
|
||||||
|
f" updated_at = ? WHERE {where}",
|
||||||
|
(now, *where_params),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif action == "rebuild":
|
||||||
|
rows = await fetch_all(
|
||||||
|
f"SELECT id FROM articles WHERE {where} LIMIT 2000", tuple(where_params)
|
||||||
|
)
|
||||||
|
for r in rows:
|
||||||
|
await _rebuild_article(r["id"])
|
||||||
|
|
||||||
|
elif action == "delete":
|
||||||
|
from ..content.routes import BUILD_DIR
|
||||||
|
|
||||||
|
rows = await fetch_all(
|
||||||
|
f"SELECT id, slug FROM articles WHERE {where} LIMIT 5000", tuple(where_params)
|
||||||
|
)
|
||||||
|
for a in rows:
|
||||||
|
build_path = BUILD_DIR / f"{a['slug']}.html"
|
||||||
|
if build_path.exists():
|
||||||
|
build_path.unlink()
|
||||||
|
md_path = Path("data/content/articles") / f"{a['slug']}.md"
|
||||||
|
if md_path.exists():
|
||||||
|
md_path.unlink()
|
||||||
|
await execute(f"DELETE FROM articles WHERE {where}", tuple(where_params))
|
||||||
|
from ..sitemap import invalidate_sitemap_cache
|
||||||
|
invalidate_sitemap_cache()
|
||||||
|
|
||||||
|
else:
|
||||||
|
ids_raw = form.get("article_ids", "").strip()
|
||||||
|
if not ids_raw:
|
||||||
|
return "", 400
|
||||||
article_ids = [int(i) for i in ids_raw.split(",") if i.strip().isdigit()]
|
article_ids = [int(i) for i in ids_raw.split(",") if i.strip().isdigit()]
|
||||||
assert len(article_ids) <= 500, "too many article IDs in bulk action"
|
assert len(article_ids) <= 500, "too many article IDs in bulk action"
|
||||||
if not article_ids:
|
if not article_ids:
|
||||||
return "", 400
|
return "", 400
|
||||||
|
|
||||||
placeholders = ",".join("?" for _ in article_ids)
|
placeholders = ",".join("?" for _ in article_ids)
|
||||||
now = utcnow_iso()
|
|
||||||
|
|
||||||
if action == "publish":
|
if action == "publish":
|
||||||
await execute(
|
await execute(
|
||||||
@@ -2557,18 +2666,17 @@ async def articles_bulk():
|
|||||||
elif action == "delete":
|
elif action == "delete":
|
||||||
from ..content.routes import BUILD_DIR
|
from ..content.routes import BUILD_DIR
|
||||||
|
|
||||||
articles = await fetch_all(
|
articles_rows = await fetch_all(
|
||||||
f"SELECT id, slug FROM articles WHERE id IN ({placeholders})",
|
f"SELECT id, slug FROM articles WHERE id IN ({placeholders})",
|
||||||
tuple(article_ids),
|
tuple(article_ids),
|
||||||
)
|
)
|
||||||
for a in articles:
|
for a in articles_rows:
|
||||||
build_path = BUILD_DIR / f"{a['slug']}.html"
|
build_path = BUILD_DIR / f"{a['slug']}.html"
|
||||||
if build_path.exists():
|
if build_path.exists():
|
||||||
build_path.unlink()
|
build_path.unlink()
|
||||||
md_path = Path("data/content/articles") / f"{a['slug']}.md"
|
md_path = Path("data/content/articles") / f"{a['slug']}.md"
|
||||||
if md_path.exists():
|
if md_path.exists():
|
||||||
md_path.unlink()
|
md_path.unlink()
|
||||||
|
|
||||||
await execute(
|
await execute(
|
||||||
f"DELETE FROM articles WHERE id IN ({placeholders})",
|
f"DELETE FROM articles WHERE id IN ({placeholders})",
|
||||||
tuple(article_ids),
|
tuple(article_ids),
|
||||||
@@ -2577,11 +2685,6 @@ async def articles_bulk():
|
|||||||
invalidate_sitemap_cache()
|
invalidate_sitemap_cache()
|
||||||
|
|
||||||
# Re-render results partial with current filters
|
# Re-render results partial with current filters
|
||||||
search = form.get("search", "").strip()
|
|
||||||
status_filter = form.get("status", "")
|
|
||||||
template_filter = form.get("template", "")
|
|
||||||
language_filter = form.get("language", "")
|
|
||||||
|
|
||||||
grouped = not language_filter
|
grouped = not language_filter
|
||||||
if grouped:
|
if grouped:
|
||||||
article_list = await _get_article_list_grouped(
|
article_list = await _get_article_list_grouped(
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
<label class="text-xs font-semibold text-slate block mb-1">Template</label>
|
<label class="text-xs font-semibold text-slate block mb-1">Template</label>
|
||||||
<select name="template" class="form-input" style="min-width:140px">
|
<select name="template" class="form-input" style="min-width:140px">
|
||||||
<option value="">All</option>
|
<option value="">All</option>
|
||||||
|
<option value="__manual__" {% if current_template == '__manual__' %}selected{% endif %}>Manual</option>
|
||||||
{% for t in template_slugs %}
|
{% for t in template_slugs %}
|
||||||
<option value="{{ t }}" {% if t == current_template %}selected{% endif %}>{{ t }}</option>
|
<option value="{{ t }}" {% if t == current_template %}selected{% endif %}>{{ t }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -75,12 +76,13 @@
|
|||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<input type="hidden" name="article_ids" id="article-bulk-ids" value="">
|
<input type="hidden" name="article_ids" id="article-bulk-ids" value="">
|
||||||
<input type="hidden" name="action" id="article-bulk-action" value="">
|
<input type="hidden" name="action" id="article-bulk-action" value="">
|
||||||
<input type="hidden" name="search" value="{{ current_search }}">
|
<input type="hidden" name="apply_to_all" id="article-bulk-apply-to-all" value="false">
|
||||||
<input type="hidden" name="status" value="{{ current_status }}">
|
<input type="hidden" name="search" id="article-bulk-search" value="{{ current_search }}">
|
||||||
<input type="hidden" name="template" value="{{ current_template }}">
|
<input type="hidden" name="status" id="article-bulk-status" value="{{ current_status }}">
|
||||||
<input type="hidden" name="language" value="{{ current_language }}">
|
<input type="hidden" name="template" id="article-bulk-template" value="{{ current_template }}">
|
||||||
|
<input type="hidden" name="language" id="article-bulk-language" value="{{ current_language }}">
|
||||||
</form>
|
</form>
|
||||||
<div id="article-bulk-bar" class="card mb-4" style="padding:0.75rem 1.25rem;display:none;align-items:center;gap:1rem;background:#EFF6FF;border:1px solid #BFDBFE;">
|
<div id="article-bulk-bar" class="card mb-4" style="padding:0.75rem 1.25rem;display:none;align-items:center;gap:1rem;flex-wrap:wrap;background:#EFF6FF;border:1px solid #BFDBFE;">
|
||||||
<span id="article-bulk-count" class="text-sm font-semibold text-navy">0 selected</span>
|
<span id="article-bulk-count" class="text-sm font-semibold text-navy">0 selected</span>
|
||||||
<select id="article-bulk-action-select" class="form-input" style="min-width:140px;padding:0.25rem 0.5rem;font-size:0.8125rem">
|
<select id="article-bulk-action-select" class="form-input" style="min-width:140px;padding:0.25rem 0.5rem;font-size:0.8125rem">
|
||||||
<option value="">Action…</option>
|
<option value="">Action…</option>
|
||||||
@@ -92,6 +94,20 @@
|
|||||||
</select>
|
</select>
|
||||||
<button type="button" class="btn btn-sm" onclick="submitArticleBulk()">Apply</button>
|
<button type="button" class="btn btn-sm" onclick="submitArticleBulk()">Apply</button>
|
||||||
<button type="button" class="btn-outline btn-sm" onclick="clearArticleSelection()">Clear</button>
|
<button type="button" class="btn-outline btn-sm" onclick="clearArticleSelection()">Clear</button>
|
||||||
|
<span id="article-select-all-banner" style="display:none;font-size:0.8125rem;color:#1E40AF;margin-left:0.5rem">
|
||||||
|
All <strong id="article-page-count"></strong> on this page selected.
|
||||||
|
<button type="button" onclick="enableApplyToAll()"
|
||||||
|
style="background:none;border:none;color:#1D4ED8;font-weight:600;cursor:pointer;text-decoration:underline;padding:0;font-size:inherit">
|
||||||
|
Select all <span id="article-matching-count">…</span> matching instead?
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span id="article-apply-to-all-banner" style="display:none;font-size:0.8125rem;color:#991B1B;font-weight:600;margin-left:0.5rem">
|
||||||
|
All matching articles selected (<span id="article-matching-count-confirm"></span> total).
|
||||||
|
<button type="button" onclick="disableApplyToAll()"
|
||||||
|
style="background:none;border:none;color:#1D4ED8;font-weight:400;cursor:pointer;text-decoration:underline;padding:0;font-size:inherit">
|
||||||
|
Undo
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Results #}
|
{# Results #}
|
||||||
@@ -101,10 +117,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const articleSelectedIds = new Set();
|
const articleSelectedIds = new Set();
|
||||||
|
let articleApplyToAll = false;
|
||||||
|
let articleMatchingCount = 0;
|
||||||
|
|
||||||
function toggleArticleSelect(id, checked) {
|
function toggleArticleSelect(id, checked) {
|
||||||
if (checked) articleSelectedIds.add(id);
|
if (checked) articleSelectedIds.add(id);
|
||||||
else articleSelectedIds.delete(id);
|
else articleSelectedIds.delete(id);
|
||||||
|
disableApplyToAll();
|
||||||
updateArticleBulkBar();
|
updateArticleBulkBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,30 +133,91 @@ function toggleArticleGroupSelect(checkbox) {
|
|||||||
if (checkbox.checked) articleSelectedIds.add(id);
|
if (checkbox.checked) articleSelectedIds.add(id);
|
||||||
else articleSelectedIds.delete(id);
|
else articleSelectedIds.delete(id);
|
||||||
});
|
});
|
||||||
|
disableApplyToAll();
|
||||||
updateArticleBulkBar();
|
updateArticleBulkBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearArticleSelection() {
|
function clearArticleSelection() {
|
||||||
articleSelectedIds.clear();
|
articleSelectedIds.clear();
|
||||||
|
articleApplyToAll = false;
|
||||||
document.querySelectorAll('.article-checkbox').forEach(function(cb) { cb.checked = false; });
|
document.querySelectorAll('.article-checkbox').forEach(function(cb) { cb.checked = false; });
|
||||||
var selectAll = document.getElementById('article-select-all');
|
var selectAll = document.getElementById('article-select-all');
|
||||||
if (selectAll) selectAll.checked = false;
|
if (selectAll) selectAll.checked = false;
|
||||||
updateArticleBulkBar();
|
updateArticleBulkBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enableApplyToAll() {
|
||||||
|
articleApplyToAll = true;
|
||||||
|
document.getElementById('article-bulk-apply-to-all').value = 'true';
|
||||||
|
document.getElementById('article-select-all-banner').style.display = 'none';
|
||||||
|
document.getElementById('article-apply-to-all-banner').style.display = 'inline';
|
||||||
|
var confirmEl = document.getElementById('article-matching-count-confirm');
|
||||||
|
if (confirmEl) confirmEl.textContent = articleMatchingCount.toLocaleString();
|
||||||
|
document.getElementById('article-bulk-count').textContent = 'All matching selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableApplyToAll() {
|
||||||
|
articleApplyToAll = false;
|
||||||
|
document.getElementById('article-bulk-apply-to-all').value = 'false';
|
||||||
|
document.getElementById('article-select-all-banner').style.display = 'none';
|
||||||
|
document.getElementById('article-apply-to-all-banner').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
function updateArticleBulkBar() {
|
function updateArticleBulkBar() {
|
||||||
var bar = document.getElementById('article-bulk-bar');
|
var bar = document.getElementById('article-bulk-bar');
|
||||||
var count = document.getElementById('article-bulk-count');
|
var countEl = document.getElementById('article-bulk-count');
|
||||||
var ids = document.getElementById('article-bulk-ids');
|
var ids = document.getElementById('article-bulk-ids');
|
||||||
bar.style.display = articleSelectedIds.size > 0 ? 'flex' : 'none';
|
|
||||||
count.textContent = articleSelectedIds.size + ' selected';
|
if (articleSelectedIds.size === 0 && !articleApplyToAll) {
|
||||||
|
bar.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bar.style.display = 'flex';
|
||||||
|
|
||||||
|
if (!articleApplyToAll) {
|
||||||
|
countEl.textContent = articleSelectedIds.size + ' selected';
|
||||||
ids.value = Array.from(articleSelectedIds).join(',');
|
ids.value = Array.from(articleSelectedIds).join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if select-all is checked → show "select all matching" banner
|
||||||
|
var selectAll = document.getElementById('article-select-all');
|
||||||
|
var allOnPage = document.querySelectorAll('.article-checkbox');
|
||||||
|
var pageCount = 0;
|
||||||
|
allOnPage.forEach(function(cb) {
|
||||||
|
if (cb.dataset.ids) {
|
||||||
|
pageCount += (cb.dataset.ids || '').split(',').filter(Boolean).length;
|
||||||
|
} else {
|
||||||
|
pageCount += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var selectAllBanner = document.getElementById('article-select-all-banner');
|
||||||
|
if (!articleApplyToAll && selectAll && selectAll.checked && pageCount > 0) {
|
||||||
|
document.getElementById('article-page-count').textContent = pageCount;
|
||||||
|
selectAllBanner.style.display = 'inline';
|
||||||
|
// Fetch count of matching articles
|
||||||
|
var params = new URLSearchParams({
|
||||||
|
search: document.getElementById('article-bulk-search').value,
|
||||||
|
status: document.getElementById('article-bulk-status').value,
|
||||||
|
template: document.getElementById('article-bulk-template').value,
|
||||||
|
language: document.getElementById('article-bulk-language').value,
|
||||||
|
});
|
||||||
|
fetch('{{ url_for("admin.articles_matching_count") }}?' + params.toString())
|
||||||
|
.then(function(r) { return r.text(); })
|
||||||
|
.then(function(text) {
|
||||||
|
articleMatchingCount = parseInt(text.replace(/,/g, ''), 10) || 0;
|
||||||
|
var el = document.getElementById('article-matching-count');
|
||||||
|
if (el) el.textContent = text;
|
||||||
|
});
|
||||||
|
} else if (!articleApplyToAll) {
|
||||||
|
selectAllBanner.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function submitArticleBulk() {
|
function submitArticleBulk() {
|
||||||
var action = document.getElementById('article-bulk-action-select').value;
|
var action = document.getElementById('article-bulk-action-select').value;
|
||||||
if (!action) return;
|
if (!action) return;
|
||||||
if (articleSelectedIds.size === 0) return;
|
if (!articleApplyToAll && articleSelectedIds.size === 0) return;
|
||||||
|
|
||||||
function doSubmit() {
|
function doSubmit() {
|
||||||
document.getElementById('article-bulk-action').value = action;
|
document.getElementById('article-bulk-action').value = action;
|
||||||
@@ -150,7 +230,13 @@ function submitArticleBulk() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'delete') {
|
if (action === 'delete') {
|
||||||
showConfirm('Delete ' + articleSelectedIds.size + ' articles? This cannot be undone.').then(function(ok) {
|
var subject = articleApplyToAll
|
||||||
|
? 'Delete all ' + articleMatchingCount.toLocaleString() + ' matching articles? This cannot be undone.'
|
||||||
|
: 'Delete ' + articleSelectedIds.size + ' articles? This cannot be undone.';
|
||||||
|
showConfirm(subject).then(function(ok) { if (ok) doSubmit(); });
|
||||||
|
} else if (articleApplyToAll) {
|
||||||
|
var verb = action.charAt(0).toUpperCase() + action.slice(1);
|
||||||
|
showConfirm(verb + ' all ' + articleMatchingCount.toLocaleString() + ' matching articles?').then(function(ok) {
|
||||||
if (ok) doSubmit();
|
if (ok) doSubmit();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -158,6 +244,30 @@ function submitArticleBulk() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync filter values into bulk form hidden inputs when filters change
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var filterForm = document.querySelector('form[hx-get*="article_results"]');
|
||||||
|
if (!filterForm) return;
|
||||||
|
filterForm.addEventListener('change', syncBulkFilters);
|
||||||
|
filterForm.addEventListener('input', syncBulkFilters);
|
||||||
|
});
|
||||||
|
|
||||||
|
function syncBulkFilters() {
|
||||||
|
var filterForm = document.querySelector('form[hx-get*="article_results"]');
|
||||||
|
if (!filterForm) return;
|
||||||
|
var fd = new FormData(filterForm);
|
||||||
|
var searchEl = document.getElementById('article-bulk-search');
|
||||||
|
var statusEl = document.getElementById('article-bulk-status');
|
||||||
|
var templateEl = document.getElementById('article-bulk-template');
|
||||||
|
var languageEl = document.getElementById('article-bulk-language');
|
||||||
|
if (searchEl) searchEl.value = fd.get('search') || '';
|
||||||
|
if (statusEl) statusEl.value = fd.get('status') || '';
|
||||||
|
if (templateEl) templateEl.value = fd.get('template') || '';
|
||||||
|
if (languageEl) languageEl.value = fd.get('language') || '';
|
||||||
|
// Changing filters clears apply-to-all and resets selection
|
||||||
|
clearArticleSelection();
|
||||||
|
}
|
||||||
|
|
||||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||||
if (evt.detail.target.id === 'article-results') {
|
if (evt.detail.target.id === 'article-results') {
|
||||||
document.querySelectorAll('.article-checkbox').forEach(function(cb) {
|
document.querySelectorAll('.article-checkbox').forEach(function(cb) {
|
||||||
|
|||||||
Reference in New Issue
Block a user