diff --git a/app/src/DashboardPage.js b/app/src/DashboardPage.js
index a62c8995..edfa85da 100644
--- a/app/src/DashboardPage.js
+++ b/app/src/DashboardPage.js
@@ -158,7 +158,7 @@ export function DashboardPage() {
const navigate = useNavigate();
const {searchStr, setSearchStr, activeTags} = useSearch();
- const {suggestions} = useSearchSuggestions();
+ const {suggestions, handleResultSelect} = useSearchSuggestions();
const {status} = useContext(StatusContext);
let title = 'Dashboard';
@@ -198,6 +198,7 @@ export function DashboardPage() {
onClear={() => navigate('/')}
style={{marginBottom: '2em'}}
results={suggestions}
+ handleResultSelect={handleResultSelect}
/>
@@ -220,6 +221,7 @@ export function DashboardPage() {
onClear={() => navigate('/')}
style={{marginBottom: '2em'}}
results={suggestions}
+ handleResultSelect={handleResultSelect}
/>
diff --git a/app/src/api.js b/app/src/api.js
index 89a72827..21d8236e 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -1026,9 +1026,11 @@ export async function searchSuggestions(search_str) {
const content = await response.json();
const channels = content['channels'];
const domains = content['domains'];
+ const tags = content['tags'];
return {
channels,
domains,
+ tags,
}
}
}
diff --git a/app/src/components/Common.js b/app/src/components/Common.js
index 7008c1a2..8a90f4bf 100644
--- a/app/src/components/Common.js
+++ b/app/src/components/Common.js
@@ -413,10 +413,10 @@ export function SearchInput({
onClear = null,
onChange = null,
results = null,
+ handleResultSelect = null,
...props
}) {
const [value, setValue] = useState(searchStr || '');
- const navigate = useNavigate();
const handleChange = (e) => {
if (e) {
@@ -455,19 +455,13 @@ export function SearchInput({
}
}
- const localHandleResultSelect = (e, {result}) => {
+ const localHandleResultSelect = (e, data) => {
if (e) {
e.preventDefault();
}
-
- if (!result) {
- return;
- }
-
- if (result['type'] === 'channel') {
- navigate(`/videos/channel/${result['id']}/video`);
- } else if (result['type'] === 'domain') {
- navigate(`/archive?domain=${result['domain']}`);
+ console.debug(`Selected result`, data);
+ if (handleResultSelect) {
+ handleResultSelect(data);
}
}
diff --git a/app/src/components/Search.js b/app/src/components/Search.js
index 4244271a..b613c7de 100644
--- a/app/src/components/Search.js
+++ b/app/src/components/Search.js
@@ -7,8 +7,19 @@ import {searchEstimate, searchSuggestions} from "../api";
import {normalizeEstimate, TabLinks} from "./Common";
import _ from "lodash";
+const suggestedApps = [
+ {location: '/more/otp', title: 'One Time Pad', description: 'Encrypt and Decrypt messages'},
+ {location: '/inventory', title: 'Inventory', description: 'Track and organize your food storage'},
+ {location: '/more/vin', title: 'Vin Decoder', description: 'Decode and analyze vehicle VIN numbers'},
+ {location: '/admin', title: 'Downloads', description: 'View your downloads'},
+ {location: '/admin/settings', title: 'Settings', description: 'View and modify your settings'},
+ {location: '/admin/wrol', title: 'WROL Mode', description: 'Enable or disable WROL Mode'},
+];
+
export function useSearchSuggestions() {
+ const navigate = useNavigate();
const {searchParams} = useQuery();
+
const searchStr = searchParams.get('q');
const [suggestions, setSuggestions] = useState(null);
@@ -17,6 +28,7 @@ export function useSearchSuggestions() {
if (searchStr && searchStr.length > 0) {
try {
const i = await searchSuggestions(searchStr);
+ const matchingApps = suggestedApps.filter(i => i.title.toLowerCase().includes(searchStr));
const newSuggestions = {
channels: {
name: 'Channels', results: i['channels'].map(i => {
@@ -28,6 +40,12 @@ export function useSearchSuggestions() {
return {title: i['domain'], id: i['id'], type: 'domain'}
})
},
+ tags: {
+ name: 'Tags', results: i['tags'].map(i => {
+ return {title: i['name'], type: 'tag'}
+ })
+ },
+ apps: {name: 'Apps', results: matchingApps},
};
console.debug('newSuggestions', newSuggestions);
setSuggestions(newSuggestions);
@@ -44,7 +62,24 @@ export function useSearchSuggestions() {
fetchSuggestions();
}, [searchStr]);
- return {suggestions, searchStr}
+
+ const handleResultSelect = ({result}) => {
+ if (!result) {
+ return;
+ }
+
+ if (result.location) {
+ navigate(result.location);
+ } else if (result['type'] === 'channel') {
+ navigate(`/videos/channel/${result['id']}/video`);
+ } else if (result['type'] === 'domain') {
+ navigate(`/archive?domain=${result['domain']}`);
+ } else if (result['type'] === 'tag') {
+ navigate(`/search?tag=${result['title']}`);
+ }
+ }
+
+ return {suggestions, searchStr, handleResultSelect}
}
export const useSearch = (defaultLimit = 48, totalPages = 0, emptySearch = false, model) => {
diff --git a/wrolpi/root_api.py b/wrolpi/root_api.py
index 176382fd..3b9771d4 100644
--- a/wrolpi/root_api.py
+++ b/wrolpi/root_api.py
@@ -553,9 +553,11 @@ async def post_search_suggestions(_: Request, body: schema.SearchSuggestionsRequ
channels = sorted(search_channels_by_name(body.search_str), key=lambda i: i.name)
domains = sorted(search_domains_by_name(body.search_str), key=lambda i: i.domain)
+ tags_ = tags.search_tags_by_name(body.search_str)
ret = dict(
channels=channels,
domains=domains,
+ tags=tags_,
)
return json_response(ret)
diff --git a/wrolpi/tags.py b/wrolpi/tags.py
index 8465448d..b6a012c4 100644
--- a/wrolpi/tags.py
+++ b/wrolpi/tags.py
@@ -3,7 +3,7 @@ import pathlib
from datetime import datetime
from typing import List, Dict, Tuple
-from sqlalchemy import Column, Integer, String, ForeignKey, BigInteger
+from sqlalchemy import Column, Integer, String, ForeignKey, BigInteger, asc
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import relationship, Session
@@ -461,3 +461,13 @@ def tag_names_to_zim_sub_select(tag_names: List[str], zim_id: int = None) -> Tup
HAVING array_agg(t.name)::TEXT[] @> %(tag_names)s::TEXT[]
'''
return stmt, params
+
+
+@optional_session
+def search_tags_by_name(name: str, limit: int = 5, session: Session = None) -> List[Tag]:
+ tags = session.query(Tag) \
+ .filter(Tag.name.ilike(f'%{name}%')) \
+ .order_by(asc(Tag.name)) \
+ .limit(limit) \
+ .all()
+ return tags
diff --git a/wrolpi/test/test_root_api.py b/wrolpi/test/test_root_api.py
index 081b2333..e9b15166 100644
--- a/wrolpi/test/test_root_api.py
+++ b/wrolpi/test/test_root_api.py
@@ -488,12 +488,13 @@ def test_search_str_estimate(test_session, test_client, test_directory, test_zim
@pytest.mark.asyncio
-async def test_search_suggestions(test_session, test_async_client, channel_factory, archive_factory):
+async def test_search_suggestions(test_session, test_async_client, channel_factory, archive_factory, tag_factory):
channel_factory(name='Foo')
channel_factory(name='Fool')
channel_factory(name='Bar')
archive_factory(domain='foo.com')
archive_factory(domain='bar.com')
+ tag_factory()
test_session.commit()
body = dict(search_str='foo')
@@ -512,6 +513,13 @@ async def test_search_suggestions(test_session, test_async_client, channel_facto
assert response.json['channels'] == [{'directory': 'Bar', 'id': 3, 'name': 'Bar', 'url': 'https://example.com/Bar'}]
assert response.json['domains'] == [{'directory': 'archive/bar.com', 'domain': 'bar.com', 'id': 2}]
+ body = dict(search_str='one')
+ request, response = await test_async_client.post('/api/search_suggestions', json=body)
+ assert response.status_code == HTTPStatus.OK
+ assert response.json['channels'] == []
+ assert response.json['domains'] == []
+ assert response.json['tags'] == [{'color': '#111111', 'id': 1, 'name': 'one'}]
+
def test_recursive_errors():
"""Errors are reported recursively."""