Add saved searches with CRUD API and UI sidebar

Add extension field to search results and display it
Add active filter badges and save button to search header
This commit is contained in:
2026-05-27 08:39:52 -04:00
parent aa2c05b05f
commit e3c25b5b09
6 changed files with 318 additions and 2 deletions
+112 -2
View File
@@ -5180,7 +5180,12 @@
} else {
snippetDiv.textContent = r.snippet || "";
}
const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [titleDiv, el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]), snippetDiv]);
const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [
el("span", { class: "search-result-ext" }, [document.createTextNode(r.extension || (r.path || "").split(".").pop() || "")]),
titleDiv,
el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]),
snippetDiv
]);
if (r.tags && r.tags.length > 0) {
const tagsDiv = el("div", { class: "search-result-tags" });
r.tags.forEach((tag) => {
@@ -5233,6 +5238,17 @@
}
header.appendChild(summaryText);
// Active filter badges
const filtersRow = el("div", { class: "search-filters-row" });
if (searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")]));
if (searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")]));
if (searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")]));
const inclEl = document.getElementById("search-include-input");
const exclEl = document.getElementById("search-exclude-input");
if (inclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("incl: " + inclEl.value.trim())]));
if (exclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("excl: " + exclEl.value.trim())]));
if (filtersRow.children.length > 0) header.appendChild(filtersRow);
// Sort controls
const sortDiv = el("div", { class: "search-sort" });
const btnRelevance = el("button", { class: "search-sort__btn" + (advancedSearchSort === "relevance" ? " active" : ""), type: "button" });
@@ -5254,6 +5270,31 @@
sortDiv.appendChild(btnRelevance);
sortDiv.appendChild(btnDate);
header.appendChild(sortDiv);
// Save search button
const saveBtn = el("button", { class: "search-save-btn", type: "button", title: "Sauvegarder cette recherche" });
saveBtn.innerHTML = '<i data-lucide="bookmark-plus" style="width:14px;height:14px"></i> Sauver';
saveBtn.addEventListener("click", async () => {
const inclEl = document.getElementById("search-include-input");
const exclEl = document.getElementById("search-exclude-input");
try {
await api("/api/saved-searches", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: query,
vault: document.getElementById("vault-filter")?.value || "all",
case_sensitive: searchCaseSensitive,
whole_word: searchWholeWord,
regex: searchRegex,
include_paths: inclEl?.value || "",
exclude_paths: exclEl?.value || "",
}),
});
showToast("Recherche sauvegardée", "success");
} catch (err) { showToast("Erreur: " + err.message, "error"); }
});
header.appendChild(saveBtn);
area.appendChild(header);
// Active sidebar tag chips
@@ -5363,7 +5404,10 @@
const vaultPath = el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path), scoreEl]);
const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [titleDiv, vaultPath, snippetDiv]);
const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [
el("span", { class: "search-result-ext" }, [document.createTextNode(r.extension || (r.path || "").split(".").pop() || "")]),
titleDiv, vaultPath, snippetDiv
]);
if (r.tags && r.tags.length > 0) {
const tagsDiv = el("div", { class: "search-result-tags" });
@@ -5995,6 +6039,72 @@
if (typeof DashboardSharedWidget !== "undefined") {
DashboardSharedWidget.load();
}
// Load saved searches sidebar
loadSavedSearches();
}
async function loadSavedSearches() {
const list = document.getElementById("saved-searches-list");
const empty = document.getElementById("saved-searches-empty");
if (!list) return;
try {
const searches = await api("/api/saved-searches");
if (!searches.length) {
list.innerHTML = "";
if (empty) empty.style.display = "";
return;
}
if (empty) empty.style.display = "none";
list.innerHTML = searches.map(s => `
<div class="saved-search-item">
<div class="saved-search-query" title="${escapeHtml(s.query)}">${escapeHtml(s.query.length > 50 ? s.query.slice(0, 50) + "..." : s.query)}</div>
<div class="saved-search-meta">
${s.case_sensitive ? '<span class="search-filter-badge">Aa</span>' : ""}
${s.whole_word ? '<span class="search-filter-badge">wd</span>' : ""}
${s.regex ? '<span class="search-filter-badge">.*</span>' : ""}
<span class="saved-search-vault">${escapeHtml(s.vault)}</span>
</div>
<button class="saved-search-delete" data-id="${s.id}" title="Supprimer">✕</button>
</div>
`).join("");
list.querySelectorAll(".saved-search-item").forEach(item => {
item.addEventListener("click", (e) => {
if (e.target.classList.contains("saved-search-delete")) return;
const idx = Array.from(list.children).indexOf(item);
const s = searches[idx];
if (!s) return;
// Apply the saved search
const input = document.getElementById("search-input");
if (input) input.value = s.query;
searchCaseSensitive = s.case_sensitive || false;
searchWholeWord = s.whole_word || false;
searchRegex = s.regex || false;
if (typeof _updateToggleUI === "function") _updateToggleUI();
if (s.include_paths) {
const incl = document.getElementById("search-include-input");
if (incl) incl.value = s.include_paths;
}
if (s.exclude_paths) {
const excl = document.getElementById("search-exclude-input");
if (excl) excl.value = s.exclude_paths;
}
// Execute the search
const vault = s.vault || "all";
if (input) { input.dispatchEvent(new Event("input")); }
clearTimeout(searchTimeout);
advancedSearchOffset = 0;
performAdvancedSearch(s.query, vault, null);
});
});
list.querySelectorAll(".saved-search-delete").forEach(b => b.addEventListener("click", async (e) => {
e.stopPropagation();
await api(`/api/saved-searches/${b.dataset.id}`, { method: "DELETE" });
loadSavedSearches();
}));
safeCreateIcons();
} catch (err) { /* silently ignore */ }
}
}
function showLoading() {