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:
+112
-2
@@ -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() {
|
||||
|
||||
@@ -320,6 +320,11 @@
|
||||
<i data-lucide="clock" style="width:13px;height:13px"></i>
|
||||
<span>Récent</span>
|
||||
</button>
|
||||
<button class="sidebar-tab" id="sidebar-tab-saved" role="tab"
|
||||
aria-selected="false" aria-controls="sidebar-panel-saved" data-tab="saved">
|
||||
<i data-lucide="bookmark" style="width:13px;height:13px"></i>
|
||||
<span>Sauvegardes</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Vaults panel -->
|
||||
@@ -356,6 +361,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Saved Searches panel -->
|
||||
<div class="sidebar-tab-panel" id="sidebar-panel-saved" role="tabpanel" aria-labelledby="sidebar-tab-saved">
|
||||
<div id="saved-searches-list" class="recent-list"></div>
|
||||
<div id="saved-searches-empty" class="recent-empty">
|
||||
<i data-lucide="bookmark" style="width:32px;height:32px"></i>
|
||||
<span>Aucune recherche sauvegardée</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!-- Sidebar resize handle -->
|
||||
|
||||
@@ -1858,7 +1858,99 @@ select {
|
||||
.search-result-item:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent);
|
||||
position: relative;
|
||||
}
|
||||
.search-result-ext {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-hover);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.search-filters-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.search-filter-badge {
|
||||
font-size: 0.65rem;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.search-filter-badge.path {
|
||||
background: rgba(5, 150, 105, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Save search button */
|
||||
.search-save-btn {
|
||||
padding: 4px 10px;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.search-save-btn:hover { background: var(--bg-hover); }
|
||||
|
||||
/* Saved searches sidebar */
|
||||
.saved-search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
.saved-search-item:hover { background: var(--bg-hover); }
|
||||
.saved-search-query {
|
||||
flex: 1;
|
||||
font-size: 0.8rem;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.saved-search-meta {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.saved-search-vault {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-muted);
|
||||
margin-left: 4px;
|
||||
}
|
||||
.saved-search-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
padding: 2px 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.saved-search-item:hover .saved-search-delete { opacity: 1; }
|
||||
.saved-search-delete:hover { color: var(--text-error); }
|
||||
.search-result-title {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 0.92rem;
|
||||
|
||||
Reference in New Issue
Block a user