diff --git a/backend/main.py b/backend/main.py index 8992abf..d0e38fb 100644 --- a/backend/main.py +++ b/backend/main.py @@ -231,6 +231,35 @@ async def api_file_save(vault_name: str, path: str = Query(..., description="Rel raise HTTPException(status_code=500, detail=f"Error saving file: {str(e)}") +@app.delete("/api/file/{vault_name}") +async def api_file_delete(vault_name: str, path: str = Query(..., description="Relative path to file")): + """Delete a file.""" + vault_data = get_vault_data(vault_name) + if not vault_data: + raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found") + + vault_root = Path(vault_data["path"]) + file_path = vault_root / path + + try: + file_path.resolve().relative_to(vault_root.resolve()) + except ValueError: + raise HTTPException(status_code=403, detail="Access denied: path outside vault") + + if not file_path.exists() or not file_path.is_file(): + raise HTTPException(status_code=404, detail=f"File not found: {path}") + + try: + file_path.unlink() + logger.info(f"File deleted: {vault_name}/{path}") + return {"status": "ok", "vault": vault_name, "path": path} + except PermissionError: + raise HTTPException(status_code=403, detail="Permission denied: vault may be read-only") + except Exception as e: + logger.error(f"Error deleting file {vault_name}/{path}: {e}") + raise HTTPException(status_code=500, detail=f"Error deleting file: {str(e)}") + + @app.get("/api/file/{vault_name}") async def api_file(vault_name: str, path: str = Query(..., description="Relative path to file")): """Return rendered HTML + metadata for a file.""" diff --git a/backend/search.py b/backend/search.py index df4b5fc..1af4f98 100644 --- a/backend/search.py +++ b/backend/search.py @@ -8,6 +8,12 @@ from backend.indexer import index, get_vault_data logger = logging.getLogger("obsigate.search") +def _normalize_tag_filter(tag_filter: Optional[str]) -> List[str]: + if not tag_filter: + return [] + return [tag.strip().lstrip("#") for tag in tag_filter.split(",") if tag.strip()] + + def _read_file_content(vault_name: str, file_path: str) -> str: """Read raw markdown content of a file from disk.""" vault_data = get_vault_data(vault_name) @@ -52,8 +58,9 @@ def search( """ query = query.strip() if query else "" has_query = len(query) > 0 + selected_tags = _normalize_tag_filter(tag_filter) - if not has_query and not tag_filter: + if not has_query and not selected_tags: return [] results: List[Dict[str, Any]] = [] @@ -63,7 +70,7 @@ def search( continue for file_info in vault_data["files"]: - if tag_filter and tag_filter not in file_info["tags"]: + if selected_tags and not all(tag in file_info["tags"] for tag in selected_tags): continue score = 0 diff --git a/frontend/app.js b/frontend/app.js index a6830a5..9916e51 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -429,47 +429,70 @@ function addTagFilter(tag) { if (!selectedTags.includes(tag)) { selectedTags.push(tag); - renderActiveTags(); performTagSearch(); } } function removeTagFilter(tag) { selectedTags = selectedTags.filter(t => t !== tag); - renderActiveTags(); if (selectedTags.length > 0) { performTagSearch(); } else { const input = document.getElementById("search-input"); - if (!input.value.trim()) { + if (input.value.trim()) { + performSearch(input.value.trim(), document.getElementById("vault-filter").value, null); + } else { showWelcome(); } } } - function renderActiveTags() { - const container = document.getElementById("active-tags"); - if (!container) return; - - container.innerHTML = ""; - - selectedTags.forEach(tag => { - const tagEl = el("div", { class: "active-tag" }, [ - document.createTextNode(`#${tag}`), - el("span", { class: "remove-icon" }, [icon("x", 14)]) - ]); - tagEl.addEventListener("click", () => removeTagFilter(tag)); - container.appendChild(tagEl); - }); - - safeCreateIcons(); - } - function performTagSearch() { const input = document.getElementById("search-input"); const query = input.value.trim(); const vault = document.getElementById("vault-filter").value; - performSearch(query, vault, selectedTags.join(",")); + performSearch(query, vault, selectedTags.length > 0 ? selectedTags.join(",") : null); + } + + function buildSearchResultsHeader(data, query, tagFilter) { + const header = el("div", { class: "search-results-header" }); + const summaryText = el("span", { class: "search-results-summary-text" }); + + if (query && tagFilter) { + summaryText.textContent = `${data.count} résultat(s) pour "${query}" avec les tags`; + } else if (query) { + summaryText.textContent = `${data.count} résultat(s) pour "${query}"`; + } else if (tagFilter) { + summaryText.textContent = `${data.count} fichier(s) avec les tags`; + } else { + summaryText.textContent = `${data.count} résultat(s)`; + } + + header.appendChild(summaryText); + + if (selectedTags.length > 0) { + const activeTags = el("div", { class: "search-results-active-tags" }); + selectedTags.forEach((tag) => { + const removeBtn = el("button", { + class: "search-results-active-tag-remove", + title: `Retirer ${tag} du filtre`, + "aria-label": `Retirer ${tag} du filtre` + }, [document.createTextNode("×")]); + removeBtn.addEventListener("click", (e) => { + e.stopPropagation(); + removeTagFilter(tag); + }); + + const chip = el("span", { class: "search-results-active-tag" }, [ + document.createTextNode(`#${tag}`), + removeBtn, + ]); + activeTags.appendChild(chip); + }); + header.appendChild(activeTags); + } + + return header; } function searchByTag(tag) { @@ -679,16 +702,7 @@ const area = document.getElementById("content-area"); area.innerHTML = ""; - const header = el("div", { class: "search-results-header" }); - if (query && tagFilter) { - const tags = tagFilter.split(',').map(t => `#${t}`).join(', '); - header.textContent = `${data.count} résultat(s) pour "${query}" avec les tags ${tags}`; - } else if (query) { - header.textContent = `${data.count} résultat(s) pour "${query}"`; - } else if (tagFilter) { - const tags = tagFilter.split(',').map(t => `#${t}`).join(', '); - header.textContent = `${data.count} fichier(s) avec les tags ${tags}`; - } + const header = buildSearchResultsHeader(data, query, tagFilter); area.appendChild(header); if (data.results.length === 0) { @@ -982,7 +996,7 @@ try { saveBtn.disabled = true; - saveBtn.innerHTML = 'Sauvegarde...'; + saveBtn.innerHTML = ''; safeCreateIcons(); const response = await fetch( @@ -999,7 +1013,7 @@ throw new Error(error.detail || "Erreur de sauvegarde"); } - saveBtn.innerHTML = 'Sauvegardé !'; + saveBtn.innerHTML = ''; safeCreateIcons(); setTimeout(() => { @@ -1017,12 +1031,48 @@ } } + async function deleteFile() { + if (!editorVault || !editorPath) return; + + const deleteBtn = document.getElementById("editor-delete"); + const originalHTML = deleteBtn.innerHTML; + + try { + deleteBtn.disabled = true; + deleteBtn.innerHTML = ''; + safeCreateIcons(); + + const response = await fetch( + `/api/file/${encodeURIComponent(editorVault)}?path=${encodeURIComponent(editorPath)}`, + { method: "DELETE" } + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || "Erreur de suppression"); + } + + closeEditor(); + showWelcome(); + await refreshSidebarForContext(); + await refreshTagsForContext(); + } catch (err) { + console.error("Delete error:", err); + alert(`Erreur: ${err.message}`); + deleteBtn.innerHTML = originalHTML; + deleteBtn.disabled = false; + safeCreateIcons(); + } + } + function initEditor() { const cancelBtn = document.getElementById("editor-cancel"); + const deleteBtn = document.getElementById("editor-delete"); const saveBtn = document.getElementById("editor-save"); const modal = document.getElementById("editor-modal"); cancelBtn.addEventListener("click", closeEditor); + deleteBtn.addEventListener("click", deleteFile); saveBtn.addEventListener("click", saveFile); // Close on overlay click diff --git a/frontend/index.html b/frontend/index.html index 7fb2d17..6bc9927 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -80,7 +80,6 @@ -