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 @@ -
@@ -157,13 +156,14 @@
Édition
- - +
diff --git a/frontend/style.css b/frontend/style.css index b03644d..607a87b 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -177,38 +177,6 @@ a:hover { pointer-events: none; } -.active-tags { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 8px; - min-height: 0; -} - -.active-tag { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 8px; - background: var(--tag-bg); - color: var(--tag-text); - border-radius: 4px; - font-family: 'JetBrains Mono', monospace; - font-size: 0.75rem; - cursor: pointer; - transition: opacity 150ms ease; -} -.active-tag:hover { - opacity: 0.7; -} -.active-tag .remove-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; -} - .header-menu { position: relative; } @@ -832,8 +800,44 @@ a:hover { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 16px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} +.search-results-summary-text { + display: inline; +} +.search-results-active-tags { + display: inline-flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; +} +.search-results-active-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + background: var(--tag-bg); + color: var(--tag-text); + border-radius: 999px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + line-height: 1.2; +} +.search-results-active-tag-remove { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border: none; + background: transparent; + color: inherit; + cursor: pointer; + padding: 0; } - .search-result-item { padding: 14px 16px; border: 1px solid var(--border); @@ -846,7 +850,6 @@ a:hover { background: var(--bg-hover); border-color: var(--accent); } - .search-result-title { font-family: 'JetBrains Mono', monospace; font-size: 0.92rem; @@ -854,20 +857,17 @@ a:hover { color: var(--text-primary); margin-bottom: 4px; } - .search-result-vault { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--accent-green); margin-bottom: 4px; } - .search-result-snippet { font-size: 0.85rem; color: var(--text-secondary); line-height: 1.5; } - .search-result-tags { margin-top: 6px; display: flex; @@ -918,7 +918,6 @@ a:hover { .editor-modal.active { display: flex; } - .editor-container { background: var(--bg-secondary); border: 1px solid var(--border); @@ -931,7 +930,6 @@ a:hover { overflow: hidden; box-shadow: 0 8px 32px rgba(0,0,0,0.4); } - .editor-header { display: flex; align-items: center; @@ -940,24 +938,24 @@ a:hover { border-bottom: 1px solid var(--border); background: var(--bg-primary); flex-shrink: 0; + position: sticky; + top: 0; + z-index: 10; } - .editor-title { font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; color: var(--text-primary); font-weight: 600; } - .editor-actions { display: flex; - gap: 8px; + gap: 6px; } - .editor-btn { - font-family: 'JetBrains Mono', monospace; - font-size: 0.8rem; - padding: 6px 12px; + width: 36px; + height: 36px; + padding: 0; border: 1px solid var(--border); border-radius: 6px; background: transparent; @@ -966,7 +964,8 @@ a:hover { transition: all 150ms ease; display: inline-flex; align-items: center; - gap: 4px; + justify-content: center; + flex: 0 0 auto; } .editor-btn:hover { color: var(--accent); @@ -980,7 +979,10 @@ a:hover { .editor-btn.primary:hover { opacity: 0.9; } - +.editor-btn.danger:hover { + color: #ff7b72; + border-color: #ff7b72; +} .editor-body { flex: 1; min-height: 0; @@ -988,13 +990,11 @@ a:hover { overflow-y: auto; overflow-x: hidden; } - .cm-editor { height: auto; min-height: 100%; font-size: 0.9rem; } - .cm-scroller { font-family: 'JetBrains Mono', monospace; overflow-y: auto !important; @@ -1003,7 +1003,6 @@ a:hover { min-height: 100%; max-width: 100%; } - .fallback-editor { width: 100%; min-height: 100%; @@ -1028,60 +1027,58 @@ a:hover { @media (max-width: 768px) { .editor-modal { padding: 0; + align-items: stretch; } - .editor-container { max-width: 100%; max-height: 100vh; border-radius: 0; height: 100vh; + overflow: hidden; } - .editor-header { padding: 10px 12px; position: sticky; top: 0; - z-index: 10; + z-index: 20; + background: var(--bg-primary); } - .editor-title { font-size: 0.85rem; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - .editor-actions { - gap: 6px; + gap: 4px; } - .editor-btn { - font-size: 0.75rem; - padding: 8px; + width: 36px; + height: 36px; min-width: 36px; } - - .editor-btn-text { - display: none; - } - .editor-body { - height: calc(100vh - 50px); + flex: 1; + min-height: 0; overflow: auto; } - .cm-editor { height: auto; min-height: 100%; } - .cm-scroller { overflow: auto !important; height: auto; min-height: 100%; } - .fallback-editor { - min-height: calc(100vh - 50px); + min-height: 100%; height: auto; } + .search-results-header { + align-items: flex-start; + } } /* --- No-select during resize --- */