From 862393b52b64340eecb68130c1aff881c033433c Mon Sep 17 00:00:00 2001 From: Thomas Halwax Date: Fri, 30 Jan 2026 19:34:59 +0100 Subject: [PATCH 1/4] intorduced a taglist that shows all tags available as a shortcut --- .../components/sidebar/FilterInput.css | 29 +++++++++ .../components/sidebar/FilterInput.js | 65 +++++++++++++++---- src/renderer/store/SearchIndex.js | 11 ++++ 3 files changed, 91 insertions(+), 14 deletions(-) diff --git a/src/renderer/components/sidebar/FilterInput.css b/src/renderer/components/sidebar/FilterInput.css index 21f6f956..95c1874e 100644 --- a/src/renderer/components/sidebar/FilterInput.css +++ b/src/renderer/components/sidebar/FilterInput.css @@ -8,4 +8,33 @@ .fe6e-filter { font-size: 100%; width: 100%; +} + +.fe6e-taglist { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; + padding: 8px; + border: 0.07rem solid var(--color-border); + border-radius: 4px; + box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px; + max-height: 7em; + overflow-y: auto; +} + +.fe6e-taglist-tag { + font-size: 0.75em; + padding: 2px 6px; + border-radius: 2px; + border: 1px solid var(--color-secondary-accent); + white-space: nowrap; + cursor: pointer; + text-transform: uppercase; +} + +.fe6e-taglist-tag:hover { + background-color: var(--color-tag-hover); + border-color: var(--color-tag-hover); + color: black; } \ No newline at end of file diff --git a/src/renderer/components/sidebar/FilterInput.js b/src/renderer/components/sidebar/FilterInput.js index 3581db34..592bd782 100644 --- a/src/renderer/components/sidebar/FilterInput.js +++ b/src/renderer/components/sidebar/FilterInput.js @@ -1,6 +1,6 @@ /* eslint-disable react/prop-types */ import React from 'react' -import { useMemento } from '../hooks' +import { useServices, useMemento } from '../hooks' import { defaultSearch } from './state' import { matcher, stopPropagation } from '../events' import { cmdOrCtrl } from '../../platform' @@ -12,8 +12,10 @@ import './FilterInput.css' * */ export const FilterInput = props => { + const { searchIndex } = useServices() const [search, setSearch] = useMemento('ui.sidebar.search', defaultSearch) const [cursor, setCursor] = React.useState(null) + const [userTags, setUserTags] = React.useState([]) const ref = React.useRef() React.useEffect(() => { @@ -25,12 +27,32 @@ export const FilterInput = props => { if (input) input.setSelectionRange(position, position) }, [ref, cursor, search.filter]) + // Fetch user tags and listen for index updates + React.useEffect(() => { + const fetchTags = async () => { + const tags = await searchIndex.userTags() + setUserTags(tags) + } + + fetchTags() + searchIndex.on('index/updated', fetchTags) + return () => searchIndex.off('index/updated', fetchTags) + }, [searchIndex]) + const handleChange = event => { const { target } = event setCursor(target.selectionStart) setSearch({ ...search, filter: target.value }) } + const handleTagClick = tag => { + const tagFilter = `#${tag}` + const newFilter = search.filter + ? `${search.filter} ${tagFilter}` + : tagFilter + setCursor(null) + setSearch({ ...search, filter: newFilter, force: true }) + } const handleKeyDown = event => { matcher([ @@ -59,18 +81,33 @@ export const FilterInput = props => { } return ( -
- -
+ <> +
+ +
+ {userTags.length > 0 && ( +
+ {userTags.map(tag => ( + handleTagClick(tag)} + > + {tag.toUpperCase()} + + ))} +
+ )} + ) } diff --git a/src/renderer/store/SearchIndex.js b/src/renderer/store/SearchIndex.js index f96e39f7..a15340dc 100644 --- a/src/renderer/store/SearchIndex.js +++ b/src/renderer/store/SearchIndex.js @@ -267,3 +267,14 @@ SearchIndex.prototype.createQuery = function (terms, callback, options) { disposable.on(this.sessionStore, 'put', refresh) return disposable } + + +/** + * Get all unique user-defined tags from the database. + * Returns sorted array of tag names. + */ +SearchIndex.prototype.userTags = async function () { + const tagArrays = await L.values(this.jsonDB, ID.TAGS_PREFIX) + const tags = tagArrays.flatMap(arr => Array.isArray(arr) ? arr : []) + return [...new Set(tags)].sort() +} From 57550887970a5527799b761881a73d24e9c09820 Mon Sep 17 00:00:00 2001 From: Thomas Halwax Date: Fri, 30 Jan 2026 19:50:04 +0100 Subject: [PATCH 2/4] allow negative filter for tags using -#TAG --- src/renderer/store/MiniSearch.js | 15 ++++++++++----- src/renderer/store/SearchIndex.js | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/renderer/store/MiniSearch.js b/src/renderer/store/MiniSearch.js index cda3fbe8..9be22bb8 100644 --- a/src/renderer/store/MiniSearch.js +++ b/src/renderer/store/MiniSearch.js @@ -40,11 +40,12 @@ export const parseQuery = (terms, ids = []) => { // Remove hyphens to match the extractField transformation for scope const scopeValue = token.substring(1).replace(/-/g, '') scopeValue.length > 1 && acc.scope.push(scopeValue) - } else if (token.startsWith('#')) token.length > 2 && acc.tags.push(token.substring(1)) + } else if (token.startsWith('-#')) token.length > 3 && acc.excludeTags.push(token.substring(2).toLowerCase()) + else if (token.startsWith('#')) token.length > 2 && acc.tags.push(token.substring(1)) else if (token.startsWith('!')) token.length > 2 && acc.ids.push(token.substring(1)) else if (token.startsWith('&')) { /* ignore */ } else if (token) acc.text.push(token) return acc - }, { scope: [], text: [], tags: [], ids }) + }, { scope: [], text: [], tags: [], excludeTags: [], ids }) const query = { combineWith: 'AND', queries: [] } @@ -58,11 +59,15 @@ export const parseQuery = (terms, ids = []) => { add('text', 'AND', true) add('tags', 'AND', true) - const filter = parts.ids && parts.ids.length + const filter = parts.ids.length ? result => parts.ids.some(id => result.id.startsWith(id)) : null - return filter - ? [query, { filter }] + const options = {} + if (filter) options.filter = filter + if (parts.excludeTags.length) options.excludeTags = parts.excludeTags + + return Object.keys(options).length + ? [query, options] : [query] } diff --git a/src/renderer/store/SearchIndex.js b/src/renderer/store/SearchIndex.js index a15340dc..72c981f7 100644 --- a/src/renderer/store/SearchIndex.js +++ b/src/renderer/store/SearchIndex.js @@ -201,7 +201,7 @@ SearchIndex.prototype.searchField = function (field, tokens) { SearchIndex.prototype.search = async function (terms, options) { if (terms.includes('@place') && options.force) { const query = terms - .replace(/([@#!&]\S+)/gi, '') + .replace(/([@#!&-]\S+)/gi, '') .trim() .replace(/[ ]+/g, '+') @@ -225,7 +225,18 @@ SearchIndex.prototype.search = async function (terms, options) { ? this.index.search(query, searchOptions) : this.index.search(query) - const keys = matches.map(R.prop('id')) + // Filter out excluded tags using cached documents + const excludeTags = searchOptions?.excludeTags || [] + const filteredMatches = excludeTags.length + ? matches.filter(match => { + const doc = this.cachedDocuments[match.id] + if (!doc || !doc.tags) return true + const docTags = doc.tags.map(t => t.toLowerCase()) + return !excludeTags.some(tag => docTags.includes(tag)) + }) + : matches + + const keys = filteredMatches.map(R.prop('id')) const option = id => { From 03c03b185765b7a05807c86be35ece8c31a72315 Mon Sep 17 00:00:00 2001 From: Thomas Halwax Date: Fri, 30 Jan 2026 19:54:26 +0100 Subject: [PATCH 3/4] Clear button to reset the search --- .../components/sidebar/FilterInput.css | 18 ++++++++++++++++-- src/renderer/components/sidebar/FilterInput.js | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/sidebar/FilterInput.css b/src/renderer/components/sidebar/FilterInput.css index 95c1874e..9613de69 100644 --- a/src/renderer/components/sidebar/FilterInput.css +++ b/src/renderer/components/sidebar/FilterInput.css @@ -1,13 +1,27 @@ .fe6e-filter-container { + display: flex; + align-items: center; border: 0.07rem solid var(--color-border); border-radius: 4px; box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px; - padding: 8px 0 8px 8px; + padding: 8px; } .fe6e-filter { font-size: 100%; - width: 100%; + flex: 1; +} + +.fe6e-filter-clear { + display: flex; + align-items: center; + cursor: pointer; + color: grey; + margin-left: 4px; +} + +.fe6e-filter-clear:hover { + color: black; } .fe6e-taglist { diff --git a/src/renderer/components/sidebar/FilterInput.js b/src/renderer/components/sidebar/FilterInput.js index 592bd782..a8ecf793 100644 --- a/src/renderer/components/sidebar/FilterInput.js +++ b/src/renderer/components/sidebar/FilterInput.js @@ -1,5 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react' +import Icon from '@mdi/react' +import { mdiClose } from '@mdi/js' import { useServices, useMemento } from '../hooks' import { defaultSearch } from './state' import { matcher, stopPropagation } from '../events' @@ -54,6 +56,13 @@ export const FilterInput = props => { setSearch({ ...search, filter: newFilter, force: true }) } + const handleClear = event => { + event.stopPropagation() + setCursor(null) + setSearch({ ...search, filter: '' }) + ref.current?.focus() + } + const handleKeyDown = event => { matcher([ ({ key }) => key === 'Enter', @@ -94,6 +103,11 @@ export const FilterInput = props => { onClick={stopPropagation} id='filter-input' /> + {search.filter && ( + + + + )} {userTags.length > 0 && (
From a7f414f94eb153c8e993a3e952b8eb41318354fd Mon Sep 17 00:00:00 2001 From: Thomas Halwax Date: Fri, 30 Jan 2026 20:11:44 +0100 Subject: [PATCH 4/4] fix: filter undefined --- src/renderer/store/SearchIndex.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/renderer/store/SearchIndex.js b/src/renderer/store/SearchIndex.js index 72c981f7..95343c19 100644 --- a/src/renderer/store/SearchIndex.js +++ b/src/renderer/store/SearchIndex.js @@ -221,8 +221,11 @@ SearchIndex.prototype.search = async function (terms, options) { )(terms) const [query, searchOptions] = parseQuery(terms, ids) - const matches = searchOptions - ? this.index.search(query, searchOptions) + + // Only pass filter option to MiniSearch, handle excludeTags separately + const miniSearchOptions = searchOptions?.filter ? { filter: searchOptions.filter } : null + const matches = miniSearchOptions + ? this.index.search(query, miniSearchOptions) : this.index.search(query) // Filter out excluded tags using cached documents @@ -231,7 +234,7 @@ SearchIndex.prototype.search = async function (terms, options) { ? matches.filter(match => { const doc = this.cachedDocuments[match.id] if (!doc || !doc.tags) return true - const docTags = doc.tags.map(t => t.toLowerCase()) + const docTags = doc.tags.filter(Boolean).map(t => t.toLowerCase()) return !excludeTags.some(tag => docTags.includes(tag)) }) : matches