Apps and tags are suggested in global search.

This commit is contained in:
Roland 2023-11-20 11:29:14 -07:00
parent d042b06e8b
commit fdfb8797fe
No known key found for this signature in database
GPG key ID: 9D6DBD3ACF11AB93
7 changed files with 68 additions and 15 deletions

View file

@ -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}
/>
</Grid.Column>
<Grid.Column width={3} textAlign='right'>
@ -220,6 +221,7 @@ export function DashboardPage() {
onClear={() => navigate('/')}
style={{marginBottom: '2em'}}
results={suggestions}
handleResultSelect={handleResultSelect}
/>
</Grid.Column>
<Grid.Column textAlign='right' width={2}>

View file

@ -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,
}
}
}

View file

@ -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);
}
}

View file

@ -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) => {

View file

@ -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)

View file

@ -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

View file

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