add filtering and sorting features; enhance events and articles pages with URL state persistence
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// articles page — list + keyword/status/source filtering + edit modal
|
||||
// depends on: app.js (api, toast, escapeHtml, PAGE, badgeHtml)
|
||||
// filter state is persisted to the url query so reload/share keeps it.
|
||||
// depends on: app.js (api, toast, escapeHtml, PAGE, badgeHtml, query*)
|
||||
|
||||
let articleOffset = 0;
|
||||
let currentArticle = null;
|
||||
@@ -8,12 +9,17 @@ let currentArticle = null;
|
||||
async function loadSources() {
|
||||
const sources = await api("/admin/api/sources");
|
||||
const sel = document.getElementById("f-source");
|
||||
|
||||
const current = queryGet("source");
|
||||
|
||||
sources.forEach(s => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = s;
|
||||
opt.textContent = s;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
|
||||
if (current) sel.value = current;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +34,16 @@ function getFilters() {
|
||||
}
|
||||
|
||||
|
||||
// sync current filters + offset into url so the state is bookmarkable
|
||||
function syncUrl() {
|
||||
const f = getFilters();
|
||||
queryWrite({
|
||||
...f,
|
||||
offset: articleOffset > 0 ? articleOffset : "",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function loadArticles() {
|
||||
const f = getFilters();
|
||||
const params = new URLSearchParams({ limit: PAGE, offset: articleOffset });
|
||||
@@ -88,14 +104,42 @@ async function openArticle(id) {
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
document.getElementById("searchBtn").onclick = () => { articleOffset = 0; loadArticles(); };
|
||||
document.getElementById("prevBtn").onclick = () => { articleOffset = Math.max(0, articleOffset - PAGE); loadArticles(); };
|
||||
document.getElementById("nextBtn").onclick = () => { articleOffset += PAGE; loadArticles(); };
|
||||
// restore filter inputs from the current url
|
||||
queryApplyToInputs({
|
||||
"f-keyword": "keyword",
|
||||
"f-status": "content_status",
|
||||
"f-from": "from",
|
||||
"f-to": "to",
|
||||
});
|
||||
articleOffset = parseInt(queryGet("offset"), 10) || 0;
|
||||
|
||||
|
||||
document.getElementById("searchBtn").onclick = () => {
|
||||
articleOffset = 0;
|
||||
syncUrl();
|
||||
loadArticles();
|
||||
};
|
||||
|
||||
document.getElementById("prevBtn").onclick = () => {
|
||||
articleOffset = Math.max(0, articleOffset - PAGE);
|
||||
syncUrl();
|
||||
loadArticles();
|
||||
};
|
||||
|
||||
document.getElementById("nextBtn").onclick = () => {
|
||||
articleOffset += PAGE;
|
||||
syncUrl();
|
||||
loadArticles();
|
||||
};
|
||||
|
||||
document.querySelector(".filters").addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") { articleOffset = 0; loadArticles(); }
|
||||
if (e.key === "Enter") {
|
||||
articleOffset = 0;
|
||||
syncUrl();
|
||||
loadArticles();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -140,6 +184,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
};
|
||||
|
||||
loadSources();
|
||||
await loadSources(); // wait for dropdown to render the saved source selection
|
||||
loadArticles();
|
||||
});
|
||||
|
||||
@@ -1,12 +1,43 @@
|
||||
// events page — list + title edit + detach-and-delete
|
||||
// events page — list + title edit + detach-and-delete, with filters
|
||||
// filter/sort/pagination state persists to url query params
|
||||
// depends on: app.js
|
||||
|
||||
let eventOffset = 0;
|
||||
let currentEvent = null;
|
||||
|
||||
|
||||
function getEventFilters() {
|
||||
return {
|
||||
keyword: document.getElementById("ef-keyword").value.trim(),
|
||||
min_articles: document.getElementById("ef-min").value.trim(),
|
||||
from: document.getElementById("ef-from").value,
|
||||
to: document.getElementById("ef-to").value,
|
||||
sort: document.getElementById("ef-sort").value,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function syncUrl() {
|
||||
const f = getEventFilters();
|
||||
queryWrite({
|
||||
...f,
|
||||
// default sort isn't worth surfacing in the url
|
||||
sort: f.sort && f.sort !== "created_desc" ? f.sort : "",
|
||||
offset: eventOffset > 0 ? eventOffset : "",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function loadEvents() {
|
||||
const data = await api(`/admin/api/events?limit=${PAGE}&offset=${eventOffset}`);
|
||||
const f = getEventFilters();
|
||||
const params = new URLSearchParams({ limit: PAGE, offset: eventOffset });
|
||||
if (f.keyword) params.set("keyword", f.keyword);
|
||||
if (f.min_articles) params.set("min_articles", f.min_articles);
|
||||
if (f.from) params.set("from", f.from + "T00:00:00");
|
||||
if (f.to) params.set("to", f.to + "T23:59:59");
|
||||
if (f.sort) params.set("sort", f.sort);
|
||||
|
||||
const data = await api(`/admin/api/events?${params}`);
|
||||
const tbody = document.getElementById("eventTable");
|
||||
|
||||
tbody.innerHTML = data.rows.map(r => `
|
||||
@@ -36,8 +67,41 @@ function openEvent(id, title) {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
document.getElementById("ePrevBtn").onclick = () => { eventOffset = Math.max(0, eventOffset - PAGE); loadEvents(); };
|
||||
document.getElementById("eNextBtn").onclick = () => { eventOffset += PAGE; loadEvents(); };
|
||||
// restore filter state from url
|
||||
queryApplyToInputs({
|
||||
"ef-keyword": "keyword",
|
||||
"ef-min": "min_articles",
|
||||
"ef-from": "from",
|
||||
"ef-to": "to",
|
||||
"ef-sort": "sort",
|
||||
});
|
||||
eventOffset = parseInt(queryGet("offset"), 10) || 0;
|
||||
|
||||
|
||||
document.getElementById("eSearchBtn").onclick = () => {
|
||||
eventOffset = 0;
|
||||
syncUrl();
|
||||
loadEvents();
|
||||
};
|
||||
|
||||
document.getElementById("ePrevBtn").onclick = () => {
|
||||
eventOffset = Math.max(0, eventOffset - PAGE);
|
||||
syncUrl();
|
||||
loadEvents();
|
||||
};
|
||||
document.getElementById("eNextBtn").onclick = () => {
|
||||
eventOffset += PAGE;
|
||||
syncUrl();
|
||||
loadEvents();
|
||||
};
|
||||
|
||||
document.querySelector(".filters").addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") {
|
||||
eventOffset = 0;
|
||||
syncUrl();
|
||||
loadEvents();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("eCancelBtn").onclick = () =>
|
||||
document.getElementById("eventOverlay").classList.remove("open");
|
||||
|
||||
@@ -86,6 +86,14 @@ let graphSearchTerm = "";
|
||||
let graphSelectedId = null;
|
||||
|
||||
|
||||
function syncGraphUrl() {
|
||||
queryWrite({
|
||||
q: graphSearchTerm,
|
||||
type: graphFilterType && graphFilterType !== "all" ? graphFilterType : "",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function loadIntelGraph() {
|
||||
const data = await api("/admin/api/intelligence/graph");
|
||||
|
||||
@@ -547,8 +555,24 @@ async function toggleEvidenceRow(row) {
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// restore search/type from url
|
||||
const urlSearch = queryGet("q");
|
||||
const urlType = queryGet("type") || "all";
|
||||
|
||||
if (urlSearch) {
|
||||
document.getElementById("graph-search").value = urlSearch;
|
||||
graphSearchTerm = urlSearch.toLowerCase();
|
||||
}
|
||||
if (urlType && urlType !== "all") {
|
||||
graphFilterType = urlType;
|
||||
document.querySelectorAll(".graph-chip").forEach(c =>
|
||||
c.classList.toggle("active", c.dataset.type === urlType)
|
||||
);
|
||||
}
|
||||
|
||||
document.getElementById("graph-search").addEventListener("input", ev => {
|
||||
graphSearchTerm = ev.target.value.trim().toLowerCase();
|
||||
syncGraphUrl();
|
||||
applyGraphFilters();
|
||||
});
|
||||
|
||||
@@ -558,6 +582,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
document.querySelectorAll(".graph-chip").forEach(c => c.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
graphFilterType = btn.dataset.type;
|
||||
syncGraphUrl();
|
||||
applyGraphFilters();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
// intelligence → knowledge table
|
||||
// filter/sort/pagination persisted via url query params
|
||||
// depends on: app.js, intel-shared.js
|
||||
|
||||
let knowledgeOffset = 0;
|
||||
|
||||
|
||||
function syncUrl() {
|
||||
const companyId = document.getElementById("i-company").value;
|
||||
const type = document.getElementById("i-type").value;
|
||||
const sort = document.getElementById("i-sort").value;
|
||||
queryWrite({
|
||||
company_id: companyId,
|
||||
type: type,
|
||||
sort: sort && sort !== "id" ? sort : "",
|
||||
offset: knowledgeOffset > 0 ? knowledgeOffset : "",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function loadKnowledge() {
|
||||
const companyId = document.getElementById("i-company").value;
|
||||
const type = document.getElementById("i-type").value;
|
||||
@@ -45,19 +59,32 @@ async function loadKnowledge() {
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
document.getElementById("iPrevBtn").onclick = () => {
|
||||
knowledgeOffset = Math.max(0, knowledgeOffset - PAGE);
|
||||
syncUrl();
|
||||
loadKnowledge();
|
||||
};
|
||||
document.getElementById("iNextBtn").onclick = () => {
|
||||
knowledgeOffset += PAGE;
|
||||
syncUrl();
|
||||
loadKnowledge();
|
||||
};
|
||||
document.getElementById("i-filter-btn").onclick = () => {
|
||||
knowledgeOffset = 0;
|
||||
syncUrl();
|
||||
loadKnowledge();
|
||||
};
|
||||
|
||||
const ok = await loadIntelStatsRow();
|
||||
if (!ok) return;
|
||||
|
||||
await loadIntelCompanies();
|
||||
|
||||
// restore from url (after dropdown is populated so company_id options exist)
|
||||
queryApplyToInputs({
|
||||
"i-company": "company_id",
|
||||
"i-type": "type",
|
||||
"i-sort": "sort",
|
||||
});
|
||||
knowledgeOffset = parseInt(queryGet("offset"), 10) || 0;
|
||||
|
||||
loadKnowledge();
|
||||
});
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
// intelligence → predictions table
|
||||
// filter/sort/pagination persisted via url query params
|
||||
// depends on: app.js, intel-shared.js
|
||||
|
||||
let predictionsOffset = 0;
|
||||
|
||||
|
||||
function syncUrl() {
|
||||
const companyId = document.getElementById("i-company").value;
|
||||
const sort = document.getElementById("i-sort").value;
|
||||
queryWrite({
|
||||
company_id: companyId,
|
||||
sort: sort && sort !== "id" ? sort : "",
|
||||
offset: predictionsOffset > 0 ? predictionsOffset : "",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function loadPredictions() {
|
||||
const companyId = document.getElementById("i-company").value;
|
||||
const sort = document.getElementById("i-sort").value;
|
||||
@@ -43,19 +55,30 @@ async function loadPredictions() {
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
document.getElementById("iPrevBtn").onclick = () => {
|
||||
predictionsOffset = Math.max(0, predictionsOffset - PAGE);
|
||||
syncUrl();
|
||||
loadPredictions();
|
||||
};
|
||||
document.getElementById("iNextBtn").onclick = () => {
|
||||
predictionsOffset += PAGE;
|
||||
syncUrl();
|
||||
loadPredictions();
|
||||
};
|
||||
document.getElementById("i-filter-btn").onclick = () => {
|
||||
predictionsOffset = 0;
|
||||
syncUrl();
|
||||
loadPredictions();
|
||||
};
|
||||
|
||||
const ok = await loadIntelStatsRow();
|
||||
if (!ok) return;
|
||||
|
||||
await loadIntelCompanies();
|
||||
|
||||
queryApplyToInputs({
|
||||
"i-company": "company_id",
|
||||
"i-sort": "sort",
|
||||
});
|
||||
predictionsOffset = parseInt(queryGet("offset"), 10) || 0;
|
||||
|
||||
loadPredictions();
|
||||
});
|
||||
|
||||
@@ -63,6 +63,16 @@ async function runSql() {
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// restore selected db from url
|
||||
const urlDb = queryGet("db");
|
||||
if (urlDb === "archive" || urlDb === "intelligence") {
|
||||
document.getElementById("sql-db").value = urlDb;
|
||||
}
|
||||
|
||||
document.getElementById("sql-db").addEventListener("change", ev => {
|
||||
queryWrite({ db: ev.target.value === "archive" ? "" : ev.target.value });
|
||||
});
|
||||
|
||||
document.getElementById("sql-run-btn").onclick = runSql;
|
||||
|
||||
document.getElementById("sql-input").addEventListener("keydown", e => {
|
||||
|
||||
@@ -30,6 +30,22 @@
|
||||
|
||||
<main class="content">
|
||||
|
||||
<div class="filters">
|
||||
<label>Keyword <input type="text" id="ef-keyword" placeholder="search title..." /></label>
|
||||
<label>Min articles <input type="text" id="ef-min" placeholder="e.g. 2" style="min-width:90px" /></label>
|
||||
<label>From <input type="date" id="ef-from" /></label>
|
||||
<label>To <input type="date" id="ef-to" /></label>
|
||||
<label>Sort
|
||||
<select id="ef-sort">
|
||||
<option value="created_desc">Newest first</option>
|
||||
<option value="created_asc">Oldest first</option>
|
||||
<option value="articles_desc">Most articles</option>
|
||||
<option value="articles_asc">Fewest articles</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary" id="eSearchBtn" style="align-self:flex-end">Filter</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
|
||||
Reference in New Issue
Block a user