Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions src/renderer/components/sidebar/FilterInput.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,54 @@
.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 {
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;
}
79 changes: 65 additions & 14 deletions src/renderer/components/sidebar/FilterInput.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable react/prop-types */
import React from 'react'
import { useMemento } from '../hooks'
import Icon from '@mdi/react'
import { mdiClose } from '@mdi/js'
import { useServices, useMemento } from '../hooks'
import { defaultSearch } from './state'
import { matcher, stopPropagation } from '../events'
import { cmdOrCtrl } from '../../platform'
Expand All @@ -12,8 +14,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(() => {
Expand All @@ -25,12 +29,39 @@ 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 handleClear = event => {
event.stopPropagation()
setCursor(null)
setSearch({ ...search, filter: '' })
ref.current?.focus()
}

const handleKeyDown = event => {
matcher([
Expand Down Expand Up @@ -59,18 +90,38 @@ export const FilterInput = props => {
}

return (
<div className='fe6e-filter-container'>
<input
className='fe6e-filter'
type='text'
ref={ref}
placeholder='Search'
value={search.filter}
onChange={handleChange}
onKeyDown={handleKeyDown}
onClick={stopPropagation}
id='filter-input'
/>
</div>
<>
<div className='fe6e-filter-container'>
<input
className='fe6e-filter'
type='text'
ref={ref}
placeholder='Search'
value={search.filter}
onChange={handleChange}
onKeyDown={handleKeyDown}
onClick={stopPropagation}
id='filter-input'
/>
{search.filter && (
<span className='fe6e-filter-clear' onClick={handleClear}>
<Icon path={mdiClose} size={0.7} />
</span>
)}
</div>
{userTags.length > 0 && (
<div className='fe6e-taglist'>
{userTags.map(tag => (
<span
key={tag}
className='fe6e-taglist-tag'
onClick={() => handleTagClick(tag)}
>
{tag.toUpperCase()}
</span>
))}
</div>
)}
</>
)
}
15 changes: 10 additions & 5 deletions src/renderer/store/MiniSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] }

Expand All @@ -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]
}
33 changes: 29 additions & 4 deletions src/renderer/store/SearchIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '+')

Expand All @@ -221,11 +221,25 @@ 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)

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.filter(Boolean).map(t => t.toLowerCase())
return !excludeTags.some(tag => docTags.includes(tag))
})
: matches

const keys = filteredMatches.map(R.prop('id'))


const option = id => {
Expand Down Expand Up @@ -267,3 +281,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()
}