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."""