From 5359ae193b7997b7c377509392f58e7b371a9f1e Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 16 Aug 2026 02:16:33 -0400 Subject: [PATCH] add SortAndFilterMenu component and integrate it into various views for enhanced sorting and filtering capabilities --- public/locales/en/labels.json | 4 +- public/locales/en/projects.json | 8 + src/components/common/SortAndFilterMenu.jsx | 256 ++++++++++++++++++++ src/views/Chores/ArchivedTasks.jsx | 53 +++- src/views/Filters/FilterView.jsx | 171 ++++++++++++- src/views/Labels/LabelView.jsx | 95 +++++++- src/views/Projects/ProjectView.jsx | 204 ++++++++++++++-- src/views/Things/ThingsView.jsx | 161 +++++++++++- 8 files changed, 914 insertions(+), 38 deletions(-) create mode 100644 src/components/common/SortAndFilterMenu.jsx diff --git a/public/locales/en/labels.json b/public/locales/en/labels.json index f02b90f..47a296c 100644 --- a/public/locales/en/labels.json +++ b/public/locales/en/labels.json @@ -9,7 +9,9 @@ "placeholder": "Search labels", "noResultsTitle": "No labels match", "noResultsDescription": "No label matches \"{{searchTerm}}\".", - "clear": "Clear search" + "noFilterResultsDescription": "No label matches the current filter.", + "clear": "Clear search", + "showAll": "Show all labels" }, "detail": { "taskCount_one": "{{count}} task", diff --git a/public/locales/en/projects.json b/public/locales/en/projects.json index 0e7eed4..19c9c9e 100644 --- a/public/locales/en/projects.json +++ b/public/locales/en/projects.json @@ -12,6 +12,14 @@ "message": "Are you sure you want to delete \"{{name}}\"? This will remove the project but keep all tasks (they'll move to the Default Project)." }, "loadError": "Failed to load projects. Please try again.", + "search": { + "placeholder": "Search projects", + "noResultsTitle": "No projects match", + "noResultsDescription": "No project matches \"{{searchTerm}}\".", + "noFilterResultsDescription": "No project matches the current filter.", + "clear": "Clear search", + "showAll": "Show all projects" + }, "blurb": "Organize your tasks into projects. Create custom workspaces to keep your tasks organized and easily accessible.", "defaultDescription": "All tasks without a specific project", "selector": { diff --git a/src/components/common/SortAndFilterMenu.jsx b/src/components/common/SortAndFilterMenu.jsx new file mode 100644 index 0000000..b6235c8 --- /dev/null +++ b/src/components/common/SortAndFilterMenu.jsx @@ -0,0 +1,256 @@ +import { ArrowDownward, ArrowUpward, Check, Sort } from '@mui/icons-material' +import { + Box, + Divider, + IconButton, + ListItemContent, + ListItemDecorator, + Menu, + MenuItem, + Radio, + Typography, +} from '@mui/joy' +import { useEffect, useRef, useState } from 'react' + +/** + * Compact sort + filter menu, meant to sit next to a search input. + * + * Props: + * sortOptions - [{ name, value }] shown under the sort header + * selectedSort - currently selected sort value + * onSortChange - (value) => void + * sortDirection - 'asc' | 'desc' + * onSortDirectionChange - (direction) => void + * filterTitle - optional header for the filter section + * filterOptions - optional [{ name, value }] rendered as radios + * selectedFilter - currently selected filter value + * onFilterChange - (value) => void + * isActive - highlights the trigger button when a non-default choice is on + */ +const SortAndFilterMenu = ({ + filterOptions, + filterTitle, + icon = , + isActive, + onFilterChange, + onSortChange, + onSortDirectionChange, + selectedFilter, + selectedSort, + sortDirection = 'asc', + sortOptions = [], + title = 'Sort by', +}) => { + const [anchorEl, setAnchorEl] = useState(null) + const menuRef = useRef(null) + const buttonRef = useRef(null) + + const handleMenuClose = () => setAnchorEl(null) + + useEffect(() => { + const handleMenuOutsideClick = event => { + if ( + menuRef.current && + !menuRef.current.contains(event.target) && + !buttonRef.current?.contains(event.target) + ) { + handleMenuClose() + } + } + + document.addEventListener('mousedown', handleMenuOutsideClick) + return () => { + document.removeEventListener('mousedown', handleMenuOutsideClick) + } + }, []) + + const SectionHeader = ({ children }) => ( + + + + {children} + + + + ) + + return ( + <> + setAnchorEl(anchorEl ? null : event.currentTarget)} + variant='outlined' + color={isActive ? 'primary' : 'neutral'} + size='sm' + sx={{ height: 32, width: 32, borderRadius: '50%', flexShrink: 0 }} + aria-label='Sort and filter options' + title='Sort & Filter' + > + {icon} + + + + {title} + + + {sortOptions.map(option => ( + { + onSortChange(option.value) + handleMenuClose() + }} + sx={{ + borderRadius: 'var(--joy-radius-sm)', + backgroundColor: + selectedSort === option.value + ? 'var(--joy-palette-primary-softBg)' + : 'transparent', + '&:hover': { + backgroundColor: + selectedSort === option.value + ? 'var(--joy-palette-primary-softBg)' + : 'var(--joy-palette-neutral-softHoverBg)', + }, + }} + > + + + + {option.name} + + {selectedSort === option.value && ( + + )} + + + + ))} + + {onSortDirectionChange && ( + <> + + + onSortDirectionChange(sortDirection === 'asc' ? 'desc' : 'asc') + } + sx={{ + borderRadius: 'var(--joy-radius-sm)', + '&:hover': { + backgroundColor: 'var(--joy-palette-neutral-softHoverBg)', + }, + }} + > + + {sortDirection === 'asc' ? ( + + ) : ( + + )} + + + + {sortDirection === 'asc' ? 'Ascending' : 'Descending'} + + + + + )} + + {filterOptions?.length > 0 && ( + <> + + {filterTitle || 'Filter'} + {filterOptions.map(option => ( + { + onFilterChange(option.value) + handleMenuClose() + }} + sx={{ + borderRadius: 'var(--joy-radius-sm)', + backgroundColor: + selectedFilter === option.value + ? 'var(--joy-palette-primary-softBg)' + : 'transparent', + '&:hover': { + backgroundColor: + selectedFilter === option.value + ? 'var(--joy-palette-primary-softBg)' + : 'var(--joy-palette-neutral-softHoverBg)', + }, + }} + > + + + + + + {option.name} + + + + ))} + + )} + + + ) +} + +export default SortAndFilterMenu diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx index 7e5a9f4..547f1e7 100644 --- a/src/views/Chores/ArchivedTasks.jsx +++ b/src/views/Chores/ArchivedTasks.jsx @@ -32,6 +32,7 @@ import { useNavigate } from 'react-router-dom' import EmptyState from '../../components/common/EmptyState' import FilterBar from '../../components/common/FilterBar' import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint' +import SortAndFilterMenu from '../../components/common/SortAndFilterMenu' import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' import { useFilter } from '../../hooks/useFilter' import { useUnArchiveChore } from '../../queries/ChoreQueries' @@ -203,11 +204,46 @@ const ArchivedTasks = () => { const { activeFilters, clearAll, - filteredData: finalChores, + filteredData: filteredByBar, hasActiveFilters, setFilter, } = useFilter(filteredChores, filterDefs) + const [sortBy, setSortBy] = useState( + () => localStorage.getItem('archivedChoresSortBy') || 'archivedAt', + ) + const [sortDirection, setSortDirection] = useState( + () => localStorage.getItem('archivedChoresSortDirection') || 'desc', + ) + + useEffect(() => { + localStorage.setItem('archivedChoresSortBy', sortBy) + localStorage.setItem('archivedChoresSortDirection', sortDirection) + }, [sortBy, sortDirection]) + + const finalChores = useMemo(() => { + const direction = sortDirection === 'desc' ? -1 : 1 + return [...filteredByBar].sort((a, b) => { + switch (sortBy) { + case 'name': + return direction * (a.name || '').localeCompare(b.name || '') + case 'priority': + return direction * ((a.priority ?? 0) - (b.priority ?? 0)) + case 'dueDate': { + const aDue = new Date(a.nextDueDate || 0).getTime() + const bDue = new Date(b.nextDueDate || 0).getTime() + return direction * (aDue - bDue) + } + case 'archivedAt': + default: { + const aDate = new Date(a.updatedAt || 0).getTime() + const bDate = new Date(b.updatedAt || 0).getTime() + return direction * (aDate - bDate) + } + } + }) + }, [filteredByBar, sortBy, sortDirection]) + useEffect(() => { const loadArchivedChores = async () => { if (!membersLoading && userProfile) { @@ -734,6 +770,21 @@ const ArchivedTasks = () => { } /> + {/* Sort Menu */} + + {/* View Mode Toggle Button */} { const [editingFilter, setEditingFilter] = useState(null) const [confirmationModel, setConfirmationModel] = useState({}) const [showMoreInfoId, setShowMoreInfoId] = useState(null) + const [searchTerm, setSearchTerm] = useState('') + const [sortBy, setSortBy] = useState( + () => localStorage.getItem('filtersSortBy') || 'smart', + ) + const [sortDirection, setSortDirection] = useState( + () => localStorage.getItem('filtersSortDirection') || 'asc', + ) + const [pinnedFilter, setPinnedFilter] = useState('all') + const searchInputRef = useRef(null) + + useEffect(() => { + localStorage.setItem('filtersSortBy', sortBy) + localStorage.setItem('filtersSortDirection', sortDirection) + }, [sortBy, sortDirection]) + // Sort filters: pinned first, then by usage count, then by last used const savedFilters = useMemo(() => { return [...filtersData].sort((a, b) => { @@ -282,6 +303,71 @@ const FilterView = () => { }) }, [filtersData]) + const visibleFilters = useMemo(() => { + if (pinnedFilter === 'pinned') { + return savedFilters.filter(filter => filter.isPinned) + } + if (pinnedFilter === 'unpinned') { + return savedFilters.filter(filter => !filter.isPinned) + } + return savedFilters + }, [pinnedFilter, savedFilters]) + + const fuse = useMemo( + () => + new Fuse(visibleFilters, { + keys: ['name', 'description'], + includeScore: true, + isCaseSensitive: false, + findAllMatches: true, + }), + [visibleFilters], + ) + + const filteredFilters = useMemo(() => { + const matched = searchTerm + ? fuse.search(searchTerm).map(result => result.item) + : visibleFilters + + const direction = sortDirection === 'desc' ? -1 : 1 + + // "Smart" keeps the pinned-then-usage order the list already arrives in. + if (sortBy === 'smart') { + return direction === -1 ? [...matched].reverse() : matched + } + + return [...matched].sort((a, b) => { + switch (sortBy) { + case 'name': + return direction * (a.name || '').localeCompare(b.name || '') + case 'usage': + return direction * ((a.usageCount || 0) - (b.usageCount || 0)) + case 'lastUsed': { + const aUsed = new Date(a.lastUsedAt || 0).getTime() + const bUsed = new Date(b.lastUsedAt || 0).getTime() + return direction * (aUsed - bUsed) + } + case 'created': { + const aCreated = new Date(a.createdAt || 0).getTime() + const bCreated = new Date(b.createdAt || 0).getTime() + return direction * (aCreated - bCreated) + } + default: + return 0 + } + }) + }, [fuse, searchTerm, visibleFilters, sortBy, sortDirection]) + + const handleSearchChange = e => { + setSearchTerm(e.target.value) + setShowMoreInfoId(null) + } + + const handleSearchClose = () => { + setSearchTerm('') + searchInputRef.current?.blur() + } + // Calculate task counts for each filter useEffect(() => { if (chores && chores.res && savedFilters.length > 0) { @@ -428,6 +514,68 @@ const FilterView = () => { + {savedFilters.length > 0 && ( + + } + endDecorator={ + searchTerm && ( + + + + ) + } + /> + { + setPinnedFilter(value) + setShowMoreInfoId(null) + }} + isActive={ + pinnedFilter !== 'all' || + sortBy !== 'smart' || + sortDirection !== 'asc' + } + /> + + )} + { onClick: handleAddFilter, }} /> + ) : filteredFilters.length === 0 ? ( + } + title='No filters match' + description={ + searchTerm + ? `No saved filter matches "${searchTerm}".` + : 'No saved filter matches the current filter.' + } + primaryAction={{ + label: searchTerm ? 'Clear search' : 'Show all filters', + onClick: () => { + handleSearchClose() + setPinnedFilter('all') + }, + }} + /> ) : ( - {savedFilters.map(filter => { + {filteredFilters.map(filter => { return ( { const [confirmationModel, setConfirmationModel] = useState({}) const [showMoreInfoId, setShowMoreInfoId] = useState(null) const [searchTerm, setSearchTerm] = useState('') + const [sortBy, setSortBy] = useState( + () => localStorage.getItem('labelsSortBy') || 'name', + ) + const [sortDirection, setSortDirection] = useState( + () => localStorage.getItem('labelsSortDirection') || 'asc', + ) + const [ownershipFilter, setOwnershipFilter] = useState('all') const searchInputRef = useRef(null) + useEffect(() => { + localStorage.setItem('labelsSortBy', sortBy) + localStorage.setItem('labelsSortDirection', sortDirection) + }, [sortBy, sortDirection]) + + const visibleLabels = useMemo(() => { + if (ownershipFilter === 'mine') { + return userLabels.filter(label => label.created_by === userProfile?.id) + } + if (ownershipFilter === 'shared') { + return userLabels.filter(label => label.created_by !== userProfile?.id) + } + return userLabels + }, [ownershipFilter, userLabels, userProfile?.id]) + const fuse = useMemo( () => - new Fuse(userLabels, { + new Fuse(visibleLabels, { keys: ['name'], includeScore: true, isCaseSensitive: false, findAllMatches: true, }), - [userLabels], + [visibleLabels], ) const filteredLabels = useMemo(() => { - if (!searchTerm) { - return userLabels - } - return fuse.search(searchTerm).map(result => result.item) - }, [fuse, searchTerm, userLabels]) + const matched = searchTerm + ? fuse.search(searchTerm).map(result => result.item) + : visibleLabels + + const direction = sortDirection === 'desc' ? -1 : 1 + return [...matched].sort((a, b) => { + switch (sortBy) { + case 'color': + return direction * (a.color || '').localeCompare(b.color || '') + case 'created': + return direction * ((a.id || 0) - (b.id || 0)) + case 'name': + default: + return direction * (a.name || '').localeCompare(b.name || '') + } + }) + }, [fuse, searchTerm, visibleLabels, sortBy, sortDirection]) const handleSearchChange = e => { setSearchTerm(e.target.value.toLowerCase()) @@ -310,7 +345,9 @@ const LabelView = () => { {userLabels.length > 0 && ( - + { ) } /> + { + setOwnershipFilter(value) + setShowMoreInfoId(null) + }} + isActive={ + ownershipFilter !== 'all' || + sortBy !== 'name' || + sortDirection !== 'asc' + } + /> )} { fullHeight icon={} title={t('search.noResultsTitle')} - description={t('search.noResultsDescription', { - searchTerm, - })} + description={ + searchTerm + ? t('search.noResultsDescription', { searchTerm }) + : t('search.noFilterResultsDescription') + } primaryAction={{ - label: t('search.clear'), - onClick: handleSearchClose, + label: searchTerm ? t('search.clear') : t('search.showAll'), + onClick: () => { + handleSearchClose() + setOwnershipFilter('all') + }, }} /> )} diff --git a/src/views/Projects/ProjectView.jsx b/src/views/Projects/ProjectView.jsx index 644e3d4..1baffde 100644 --- a/src/views/Projects/ProjectView.jsx +++ b/src/views/Projects/ProjectView.jsx @@ -7,7 +7,14 @@ import { TrailingActions, Type as ListType, } from '@meauxt/react-swipeable-list' -import { Add, MoreVert, Task } from '@mui/icons-material' +import { + Add, + Close, + MoreVert, + Search, + SearchOff, + Task, +} from '@mui/icons-material' import DeleteIcon from '@mui/icons-material/Delete' import EditIcon from '@mui/icons-material/Edit' import { @@ -17,14 +24,18 @@ import { CircularProgress, Container, IconButton, + Input, Stack, Typography, } from '@mui/joy' import { useQueryClient } from '@tanstack/react-query' -import { useEffect, useState } from 'react' +import Fuse from 'fuse.js' +import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate, useSearchParams } from 'react-router-dom' +import EmptyState from '../../components/common/EmptyState' +import SortAndFilterMenu from '../../components/common/SortAndFilterMenu' import { useChores } from '../../queries/ChoreQueries' import { useUserProfile } from '../../queries/UserQueries' import { getTextColorFromBackgroundColor } from '../../utils/Colors' @@ -240,6 +251,83 @@ const ProjectView = () => { const queryClient = useQueryClient() const [confirmationModel, setConfirmationModel] = useState({}) const [showMoreInfoId, setShowMoreInfoId] = useState(null) + const [searchTerm, setSearchTerm] = useState('') + const [sortBy, setSortBy] = useState( + () => localStorage.getItem('projectsSortBy') || 'name', + ) + const [sortDirection, setSortDirection] = useState( + () => localStorage.getItem('projectsSortDirection') || 'asc', + ) + const [ownershipFilter, setOwnershipFilter] = useState('all') + const searchInputRef = useRef(null) + + useEffect(() => { + localStorage.setItem('projectsSortBy', sortBy) + localStorage.setItem('projectsSortDirection', sortDirection) + }, [sortBy, sortDirection]) + + const visibleProjects = useMemo(() => { + if (ownershipFilter === 'mine') { + return userProjects.filter( + project => project.created_by === userProfile?.id, + ) + } + if (ownershipFilter === 'shared') { + return userProjects.filter( + project => project.created_by !== userProfile?.id, + ) + } + return userProjects + }, [ownershipFilter, userProjects, userProfile?.id]) + + const fuse = useMemo( + () => + new Fuse(visibleProjects, { + keys: ['name', 'description'], + includeScore: true, + isCaseSensitive: false, + findAllMatches: true, + }), + [visibleProjects], + ) + + const filteredProjects = useMemo(() => { + const matched = searchTerm + ? fuse.search(searchTerm).map(result => result.item) + : visibleProjects + + const direction = sortDirection === 'desc' ? -1 : 1 + return [...matched].sort((a, b) => { + switch (sortBy) { + case 'tasks': + return direction * ((taskCounts[a.id] || 0) - (taskCounts[b.id] || 0)) + case 'created': + return direction * ((a.id || 0) - (b.id || 0)) + case 'name': + default: + return direction * (a.name || '').localeCompare(b.name || '') + } + }) + }, [fuse, searchTerm, visibleProjects, sortBy, sortDirection, taskCounts]) + + // The default project is pinned above the list, so it is matched separately. + const showDefaultProject = useMemo(() => { + if (ownershipFilter === 'shared') return false + if (!searchTerm) return true + return t('chores:toolbar.defaultProject') + .toLowerCase() + .includes(searchTerm.toLowerCase()) + }, [ownershipFilter, searchTerm, t]) + + const handleSearchChange = e => { + setSearchTerm(e.target.value) + setShowMoreInfoId(null) + } + + const handleSearchClose = () => { + setSearchTerm('') + searchInputRef.current?.blur() + } const handleAddProject = () => { setCurrentProject(null) @@ -388,36 +476,114 @@ const ProjectView = () => { + + } + endDecorator={ + searchTerm && ( + + + + ) + } + /> + { + setOwnershipFilter(value) + setShowMoreInfoId(null) + }} + isActive={ + ownershipFilter !== 'all' || + sortBy !== 'name' || + sortDirection !== 'asc' + } + /> + + + {!showDefaultProject && filteredProjects.length === 0 && ( + } + title={t('search.noResultsTitle')} + description={ + searchTerm + ? t('search.noResultsDescription', { searchTerm }) + : t('search.noFilterResultsDescription') + } + primaryAction={{ + label: searchTerm ? t('search.clear') : t('search.showAll'), + onClick: () => { + handleSearchClose() + setOwnershipFilter('all') + }, + }} + /> + )} {/* Default project - not swipeable */} - - handleCardClick({ + {showDefaultProject && ( + + created_by: userProfile?.id, + }} + currentUserId={userProfile?.id} + taskCounts={{ default: taskCounts.default || 0 }} + onCardClick={() => + handleCardClick({ + id: 'default', + name: t('chores:toolbar.defaultProject'), + icon: 'FolderOpen', + color: '#1976d2', + }) + } + /> + )} {/* User projects - swipeable */} - {userProjects.map(project => ( + {filteredProjects.map(project => ( handleCardClick(project)} key={project.id} diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx index 2136bbc..68d13aa 100644 --- a/src/views/Things/ThingsView.jsx +++ b/src/views/Things/ThingsView.jsx @@ -9,11 +9,14 @@ import { } from '@meauxt/react-swipeable-list' import { Add, + Close, Delete, Edit, Flip, MoreVert, PlusOne, + Search, + SearchOff, ToggleOff, ToggleOn, Widgets, @@ -24,14 +27,17 @@ import { Chip, Container, IconButton, + Input, Stack, Typography, } from '@mui/joy' -import { useEffect, useState } from 'react' +import Fuse from 'fuse.js' +import { useEffect, useMemo, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { track } from '../../analytics' import EmptyState from '../../components/common/EmptyState' +import SortAndFilterMenu from '../../components/common/SortAndFilterMenu' import { useNotification } from '../../service/NotificationProvider' import { CreateThing, @@ -215,8 +221,78 @@ const ThingsView = () => { const [createModalThing, setCreateModalThing] = useState(null) const [confirmModelConfig, setConfirmModelConfig] = useState({}) const [showMoreInfoId, setShowMoreInfoId] = useState(null) + const [searchTerm, setSearchTerm] = useState('') + const [sortBy, setSortBy] = useState( + () => localStorage.getItem('thingsSortBy') || 'name', + ) + const [sortDirection, setSortDirection] = useState( + () => localStorage.getItem('thingsSortDirection') || 'asc', + ) + const [typeFilter, setTypeFilter] = useState('all') + const searchInputRef = useRef(null) const { showError, showNotification } = useNotification() + useEffect(() => { + localStorage.setItem('thingsSortBy', sortBy) + localStorage.setItem('thingsSortDirection', sortDirection) + }, [sortBy, sortDirection]) + + const visibleThings = useMemo( + () => + typeFilter === 'all' + ? things + : things.filter(thing => thing?.type === typeFilter), + [things, typeFilter], + ) + + const fuse = useMemo( + () => + new Fuse(visibleThings, { + keys: ['name', 'state'], + includeScore: true, + isCaseSensitive: false, + findAllMatches: true, + }), + [visibleThings], + ) + + const filteredThings = useMemo(() => { + const matched = searchTerm + ? fuse.search(searchTerm).map(result => result.item) + : visibleThings + + const direction = sortDirection === 'desc' ? -1 : 1 + return [...matched].sort((a, b) => { + switch (sortBy) { + case 'type': + return direction * (a.type || '').localeCompare(b.type || '') + case 'state': + return ( + direction * + String(a.state ?? '').localeCompare(String(b.state ?? '')) + ) + case 'updated': { + const aDate = new Date(a.updatedAt || a.updated_at || 0).getTime() + const bDate = new Date(b.updatedAt || b.updated_at || 0).getTime() + return direction * (aDate - bDate) + } + case 'name': + default: + return direction * (a.name || '').localeCompare(b.name || '') + } + }) + }, [fuse, searchTerm, visibleThings, sortBy, sortDirection]) + + const handleSearchChange = e => { + setSearchTerm(e.target.value) + setShowMoreInfoId(null) + } + + const handleSearchClose = () => { + setSearchTerm('') + searchInputRef.current?.blur() + } + useEffect(() => { // fetch things GetThings().then(result => { @@ -404,6 +480,67 @@ const ThingsView = () => { + {things.length > 0 && ( + + } + endDecorator={ + searchTerm && ( + + + + ) + } + /> + { + setTypeFilter(value) + setShowMoreInfoId(null) + }} + isActive={ + typeFilter !== 'all' || + sortBy !== 'name' || + sortDirection !== 'asc' + } + /> + + )} { }} /> )} + {things.length > 0 && filteredThings.length === 0 && ( + } + title='No things match' + description={ + searchTerm + ? `No thing matches "${searchTerm}".` + : 'No thing matches the current filter.' + } + primaryAction={{ + label: searchTerm ? 'Clear search' : 'Show all things', + onClick: () => { + handleSearchClose() + setTypeFilter('all') + }, + }} + /> + )} - {things.map(thing => ( + {filteredThings.map(thing => ( navigate(`/things/${thing?.id}`)} key={thing.id}