Add tag filtering system with configurable pattern-based filters and persistent localStorage settings
This commit is contained in:
parent
7a53e85e3d
commit
0ff888280a
234
frontend/app.js
234
frontend/app.js
@ -427,7 +427,8 @@
|
|||||||
async function refreshTagsForContext() {
|
async function refreshTagsForContext() {
|
||||||
const vaultParam = selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(selectedContextVault)}`;
|
const vaultParam = selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(selectedContextVault)}`;
|
||||||
const data = await api(`/api/tags${vaultParam}`);
|
const data = await api(`/api/tags${vaultParam}`);
|
||||||
renderTagCloud(data.tags);
|
const filteredTags = TagFilterService.filterTags(data.tags);
|
||||||
|
renderTagCloud(filteredTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -673,12 +674,73 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tag Filter Service
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const TagFilterService = {
|
||||||
|
defaultFilters: [
|
||||||
|
{ pattern: "#<% ... %>", regex: "^#<%.*%>$", enabled: true },
|
||||||
|
{ pattern: "#{{ ... }}", regex: "^#\\{\\{.*\\}\\}$", enabled: true }
|
||||||
|
],
|
||||||
|
|
||||||
|
getConfig() {
|
||||||
|
const stored = localStorage.getItem("obsigate-tag-filters");
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
} catch (e) {
|
||||||
|
return { tagFilters: this.defaultFilters };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { tagFilters: this.defaultFilters };
|
||||||
|
},
|
||||||
|
|
||||||
|
saveConfig(config) {
|
||||||
|
localStorage.setItem("obsigate-tag-filters", JSON.stringify(config));
|
||||||
|
},
|
||||||
|
|
||||||
|
patternToRegex(pattern) {
|
||||||
|
let regex = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
regex = regex.replace(/\\\.\\\.\\\./g, '.*');
|
||||||
|
return regex;
|
||||||
|
},
|
||||||
|
|
||||||
|
isTagFiltered(tag) {
|
||||||
|
const config = this.getConfig();
|
||||||
|
const filters = config.tagFilters || this.defaultFilters;
|
||||||
|
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (!filter.enabled) continue;
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(`^${filter.regex}$`);
|
||||||
|
if (regex.test(`#${tag}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Invalid regex:", filter.regex, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
filterTags(tags) {
|
||||||
|
const filtered = {};
|
||||||
|
Object.entries(tags).forEach(([tag, count]) => {
|
||||||
|
if (!this.isTagFiltered(tag)) {
|
||||||
|
filtered[tag] = count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tags
|
// Tags
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
async function loadTags() {
|
async function loadTags() {
|
||||||
const data = await api("/api/tags");
|
const data = await api("/api/tags");
|
||||||
renderTagCloud(data.tags);
|
const filteredTags = TagFilterService.filterTags(data.tags);
|
||||||
|
renderTagCloud(filteredTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTagCloud(tags) {
|
function renderTagCloud(tags) {
|
||||||
@ -828,9 +890,11 @@
|
|||||||
// Tags
|
// Tags
|
||||||
const tagsDiv = el("div", { class: "file-tags" });
|
const tagsDiv = el("div", { class: "file-tags" });
|
||||||
(data.tags || []).forEach((tag) => {
|
(data.tags || []).forEach((tag) => {
|
||||||
const t = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
|
if (!TagFilterService.isTagFiltered(tag)) {
|
||||||
t.addEventListener("click", () => searchByTag(tag));
|
const t = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
|
||||||
tagsDiv.appendChild(t);
|
t.addEventListener("click", () => searchByTag(tag));
|
||||||
|
tagsDiv.appendChild(t);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
@ -1026,6 +1090,147 @@
|
|||||||
if (modal) modal.classList.remove("active");
|
if (modal) modal.classList.remove("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initConfigModal() {
|
||||||
|
const openBtn = document.getElementById("config-open-btn");
|
||||||
|
const closeBtn = document.getElementById("config-close");
|
||||||
|
const modal = document.getElementById("config-modal");
|
||||||
|
const addBtn = document.getElementById("config-add-btn");
|
||||||
|
const patternInput = document.getElementById("config-pattern-input");
|
||||||
|
|
||||||
|
if (!openBtn || !closeBtn || !modal) return;
|
||||||
|
|
||||||
|
openBtn.addEventListener("click", () => {
|
||||||
|
modal.classList.add("active");
|
||||||
|
closeHeaderMenu();
|
||||||
|
renderConfigFilters();
|
||||||
|
safeCreateIcons();
|
||||||
|
});
|
||||||
|
|
||||||
|
closeBtn.addEventListener("click", closeConfigModal);
|
||||||
|
modal.addEventListener("click", (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
closeConfigModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addBtn.addEventListener("click", addConfigFilter);
|
||||||
|
patternInput.addEventListener("keypress", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
addConfigFilter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
patternInput.addEventListener("input", updateRegexPreview);
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && modal.classList.contains("active")) {
|
||||||
|
closeConfigModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeConfigModal() {
|
||||||
|
const modal = document.getElementById("config-modal");
|
||||||
|
if (modal) modal.classList.remove("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderConfigFilters() {
|
||||||
|
const config = TagFilterService.getConfig();
|
||||||
|
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||||||
|
const container = document.getElementById("config-filters-list");
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
filters.forEach((filter, index) => {
|
||||||
|
const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [
|
||||||
|
el("span", {}, [document.createTextNode(filter.pattern)]),
|
||||||
|
el("button", {
|
||||||
|
class: "config-filter-toggle",
|
||||||
|
title: filter.enabled ? "Désactiver" : "Activer",
|
||||||
|
type: "button"
|
||||||
|
}, [document.createTextNode(filter.enabled ? "✓" : "○")]),
|
||||||
|
el("button", {
|
||||||
|
class: "config-filter-remove",
|
||||||
|
title: "Supprimer",
|
||||||
|
type: "button"
|
||||||
|
}, [document.createTextNode("×")]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const toggleBtn = badge.querySelector(".config-filter-toggle");
|
||||||
|
const removeBtn = badge.querySelector(".config-filter-remove");
|
||||||
|
|
||||||
|
toggleBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleConfigFilter(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
removeBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeConfigFilter(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(badge);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleConfigFilter(index) {
|
||||||
|
const config = TagFilterService.getConfig();
|
||||||
|
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||||||
|
if (filters[index]) {
|
||||||
|
filters[index].enabled = !filters[index].enabled;
|
||||||
|
config.tagFilters = filters;
|
||||||
|
TagFilterService.saveConfig(config);
|
||||||
|
renderConfigFilters();
|
||||||
|
refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeConfigFilter(index) {
|
||||||
|
const config = TagFilterService.getConfig();
|
||||||
|
let filters = config.tagFilters || TagFilterService.defaultFilters;
|
||||||
|
filters = filters.filter((_, i) => i !== index);
|
||||||
|
config.tagFilters = filters;
|
||||||
|
TagFilterService.saveConfig(config);
|
||||||
|
renderConfigFilters();
|
||||||
|
refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addConfigFilter() {
|
||||||
|
const input = document.getElementById("config-pattern-input");
|
||||||
|
const pattern = input.value.trim();
|
||||||
|
|
||||||
|
if (!pattern) return;
|
||||||
|
|
||||||
|
const regex = TagFilterService.patternToRegex(pattern);
|
||||||
|
const config = TagFilterService.getConfig();
|
||||||
|
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||||||
|
|
||||||
|
const newFilter = { pattern, regex, enabled: true };
|
||||||
|
filters.push(newFilter);
|
||||||
|
config.tagFilters = filters;
|
||||||
|
TagFilterService.saveConfig(config);
|
||||||
|
|
||||||
|
input.value = "";
|
||||||
|
renderConfigFilters();
|
||||||
|
refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err));
|
||||||
|
updateRegexPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRegexPreview() {
|
||||||
|
const input = document.getElementById("config-pattern-input");
|
||||||
|
const preview = document.getElementById("config-regex-preview");
|
||||||
|
const code = document.getElementById("config-regex-code");
|
||||||
|
const pattern = input.value.trim();
|
||||||
|
|
||||||
|
if (pattern) {
|
||||||
|
const regex = TagFilterService.patternToRegex(pattern);
|
||||||
|
code.textContent = `^${regex}$`;
|
||||||
|
preview.style.display = "block";
|
||||||
|
} else {
|
||||||
|
preview.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Search
|
// Search
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -1081,14 +1286,18 @@
|
|||||||
if (r.tags && r.tags.length > 0) {
|
if (r.tags && r.tags.length > 0) {
|
||||||
const tagsDiv = el("div", { class: "search-result-tags" });
|
const tagsDiv = el("div", { class: "search-result-tags" });
|
||||||
r.tags.forEach((tag) => {
|
r.tags.forEach((tag) => {
|
||||||
const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
|
if (!TagFilterService.isTagFiltered(tag)) {
|
||||||
tagEl.addEventListener("click", (e) => {
|
const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
|
||||||
e.stopPropagation();
|
tagEl.addEventListener("click", (e) => {
|
||||||
addTagFilter(tag);
|
e.stopPropagation();
|
||||||
});
|
addTagFilter(tag);
|
||||||
tagsDiv.appendChild(tagEl);
|
});
|
||||||
|
tagsDiv.appendChild(tagEl);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
item.appendChild(tagsDiv);
|
if (tagsDiv.children.length > 0) {
|
||||||
|
item.appendChild(tagsDiv);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item.addEventListener("click", () => openFile(r.vault, r.path));
|
item.addEventListener("click", () => openFile(r.vault, r.path));
|
||||||
@ -1473,6 +1682,7 @@
|
|||||||
initVaultContext();
|
initVaultContext();
|
||||||
initCollapsiblePanels();
|
initCollapsiblePanels();
|
||||||
initHelpModal();
|
initHelpModal();
|
||||||
|
initConfigModal();
|
||||||
initSidebarFilter();
|
initSidebarFilter();
|
||||||
initSidebarResize();
|
initSidebarResize();
|
||||||
initTagResize();
|
initTagResize();
|
||||||
|
|||||||
@ -116,6 +116,15 @@
|
|||||||
<span class="menu-list-subtitle" id="theme-label">Clair</span>
|
<span class="menu-list-subtitle" id="theme-label">Clair</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="menu-list-row menu-list-button" id="config-open-btn" type="button" role="menuitem">
|
||||||
|
<span class="menu-list-icon" aria-hidden="true">
|
||||||
|
<i data-lucide="settings" style="width:17px;height:17px"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-list-content">
|
||||||
|
<span class="menu-list-title">Configurations</span>
|
||||||
|
<span class="menu-list-subtitle">Paramètres de l'application</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button class="menu-list-row menu-list-button" id="help-open-btn" type="button" role="menuitem">
|
<button class="menu-list-row menu-list-button" id="help-open-btn" type="button" role="menuitem">
|
||||||
<span class="menu-list-icon" aria-hidden="true">
|
<span class="menu-list-icon" aria-hidden="true">
|
||||||
<i data-lucide="circle-help" style="width:17px;height:17px"></i>
|
<i data-lucide="circle-help" style="width:17px;height:17px"></i>
|
||||||
@ -215,6 +224,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Configurations Modal -->
|
||||||
|
<div class="editor-modal" id="config-modal">
|
||||||
|
<div class="editor-container">
|
||||||
|
<div class="editor-header">
|
||||||
|
<div class="editor-title">Configurations</div>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<button class="editor-btn" id="config-close" title="Fermer" aria-label="Fermer">
|
||||||
|
<i data-lucide="x" style="width:16px;height:16px"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-body" id="config-body">
|
||||||
|
<div class="config-content">
|
||||||
|
<section class="config-section">
|
||||||
|
<h2>Filtrage de tags</h2>
|
||||||
|
<p class="config-description">Définissez les patterns de tags à masquer dans la sidebar. Vous pouvez utiliser des wildcards pour cibler les tags de template.</p>
|
||||||
|
|
||||||
|
<div class="config-filters-list" id="config-filters-list"></div>
|
||||||
|
|
||||||
|
<div class="config-add-pattern">
|
||||||
|
<input type="text" id="config-pattern-input" placeholder="Ex: #<% ... %> ou #{{ ... }}" class="config-input">
|
||||||
|
<button id="config-add-btn" class="config-btn-add">Ajouter</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-regex-preview" id="config-regex-preview" style="display:none;">
|
||||||
|
<small>Regex : <code id="config-regex-code"></code></small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Help Modal -->
|
<!-- Help Modal -->
|
||||||
<div class="editor-modal" id="help-modal">
|
<div class="editor-modal" id="help-modal">
|
||||||
<div class="editor-container help-container">
|
<div class="editor-container help-container">
|
||||||
|
|||||||
@ -1810,3 +1810,144 @@ body.resizing-v {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Configuration Modal --- */
|
||||||
|
.config-content {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 24px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section h2 {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-description {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-filters-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-filter-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--tag-bg);
|
||||||
|
color: var(--tag-text);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--tag-text) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-filter-badge.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-filter-remove {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-filter-remove:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-filter-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-add-pattern {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn-add {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn-add:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-regex-preview {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-regex-preview code {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--accent);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user