Implement global search feature with context, UI components, and routing updates

This commit is contained in:
Mo Tarbin
2026-08-10 00:16:38 -04:00
parent 0215a78404
commit b7bdfa7885
11 changed files with 959 additions and 19 deletions

View File

@@ -411,14 +411,29 @@ const MyChores = () => {
}
}, [searchInputFocus])
// A global-search result can hand a query back to the task list as a scoped filter.
useEffect(() => {
const query = searchParams.get('search')
if (query !== null) {
setSearchTerm(query.toLowerCase())
setSelectedCalendarDate(null)
clearActiveFilter()
clearQuickFilters()
}
// The setters above are intentionally applied only when URL search params change.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams])
// Read and apply project from URL parameters
useEffect(() => {
if (!projects.length) return
const projectIdFromUrl = searchParams.get('project')
if (projectIdFromUrl && projectIdFromUrl !== selectedProject?.id) {
const project = projectsWithDefault.find(p => p.id === projectIdFromUrl)
if (projectIdFromUrl && projectIdFromUrl !== String(selectedProject?.id)) {
const project = projectsWithDefault.find(
p => String(p.id) === projectIdFromUrl,
)
if (project) {
setSelectedProjectWithCache(project)
}
@@ -759,6 +774,11 @@ const MyChores = () => {
setFilteredChores(selectedProject ? projectFilteredChores : chores)
setSearchInputFocus(0)
setSelectedCalendarDate(null)
if (searchParams.has('search')) {
const params = new URLSearchParams(searchParams)
params.delete('search')
setSearchParams(params, { replace: true })
}
}
const setSelectedChoreSectionWithCache = value => {

View File

@@ -1,21 +1,33 @@
import { CancelRounded } from '@mui/icons-material'
import { CancelRounded, SearchRounded } from '@mui/icons-material'
import { Box, Input } from '@mui/joy'
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
import { useGlobalSearch } from '../../../search/GlobalSearchContext'
const SearchBar = ({
value,
inputRef,
onChange,
onClose,
onFocus,
showKeyboardShortcuts,
inputRef,
value,
}) => {
const { openSearch } = useGlobalSearch()
const handleOpen = () => {
onFocus?.()
openSearch(value)
}
return (
<Input
slotProps={{ input: { ref: inputRef } }}
placeholder='Search'
slotProps={{ input: { ref: inputRef, readOnly: true } }}
placeholder='Search Donetick'
value={value}
onFocus={onFocus}
onFocus={handleOpen}
onMouseDown={event => {
event.preventDefault()
handleOpen()
}}
fullWidth
sx={{
mt: 1,
@@ -24,17 +36,29 @@ const SearchBar = ({
height: 24,
borderColor: 'text.disabled',
padding: 1,
cursor: 'pointer',
'& input': { cursor: 'pointer' },
}}
onChange={onChange}
startDecorator={
<KeyboardShortcutHint shortcut='F' show={showKeyboardShortcuts} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<SearchRounded sx={{ fontSize: 18, color: 'text.secondary' }} />
<KeyboardShortcutHint shortcut='F' show={showKeyboardShortcuts} />
</Box>
}
endDecorator={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{value && (
<>
<KeyboardShortcutHint shortcut='X' show={showKeyboardShortcuts} />
<CancelRounded onClick={onClose} />
<CancelRounded
aria-label='Clear task search'
onMouseDown={event => event.stopPropagation()}
onClick={event => {
event.stopPropagation()
onClose()
}}
/>
</>
)}
</Box>

View File

@@ -9,6 +9,7 @@ import {
ListAlt,
Logout,
MenuRounded,
SearchRounded,
SettingsOutlined,
Toll,
Widgets,
@@ -23,30 +24,36 @@ import {
ListItemDecorator,
Typography,
} from '@mui/joy'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { version } from '../../../package.json'
import UserProfileAvatar from '../../components/UserProfileAvatar'
import Z_INDEX from '../../constants/zIndex'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useResource } from '../../queries/ResourceQueries'
import { useGlobalSearch } from '../../search/GlobalSearchContext'
import { apiClient } from '../../utils/ApiClient'
import NavBarLink from './NavBarLink'
import SyncStatusIndicator from './SyncStatusIndicator'
import Z_INDEX from '../../constants/zIndex'
import { useResource } from '../../queries/ResourceQueries'
import { apiClient } from '../../utils/ApiClient'
const publicPages = ['/landing', '/privacy', '/terms']
const NavBar = () => {
const { t } = useTranslation('common')
const { isRTL } = useLocalization()
const { data: resource } = useResource()
const { openSearch } = useGlobalSearch()
const navigate = useNavigate()
const [drawerOpen, setDrawerOpen] = useState(false)
const links = [
{
label: t('navigation.search'),
icon: <SearchRounded />,
onClick: () => openSearch(),
},
{
to: '/chores',
label: t('navigation.allTasks'),
@@ -105,6 +112,22 @@ const NavBar = () => {
<MenuRounded />
</IconButton>
)
if (location.pathname === '/search') {
return (
<IconButton
size='md'
variant='plain'
onClick={() => {
if (window.history.state?.idx > 0) navigate(-1)
else navigate('/chores', { replace: true })
}}
aria-label='Back from search'
title={t('back')}
>
<ArrowBack />
</IconButton>
)
}
if (!Capacitor.isNativePlatform()) {
return menuRounded
}

View File

@@ -7,13 +7,12 @@ import {
import { Link } from 'react-router-dom'
const NavBarLink = ({ link }) => {
const { to, icon, label } = link
const { to, icon, label, onClick } = link
return (
<ListItem>
<ListItemButton
key={to}
component={Link}
to={to}
{...(onClick ? { onClick } : { component: Link, to })}
variant='plain'
color='neutral'
sx={{