Refactor chore filtering and search performance (#195)
* Refactor assignment strategy handling in ChoreEdit component * performance improvement : Refactor chore filtering logic and improve search functionality in MyChores and useChoreFilters hooks. reduce rerender etc... * making sure the filter clear when it supposed to * formating
This commit is contained in:
@@ -2,21 +2,21 @@ import { Add, Close } from '@mui/icons-material'
|
||||
import { Box, Button, Chip, ChipDelete, Typography } from '@mui/joy'
|
||||
|
||||
const ActiveFilterChips = ({
|
||||
chips = [],
|
||||
onOpen,
|
||||
onClearAll,
|
||||
onAdd,
|
||||
showAddChip = false,
|
||||
resultCount,
|
||||
totalCount,
|
||||
maxVisible = 2,
|
||||
chipSize = 'md',
|
||||
chipSx,
|
||||
chips = [],
|
||||
clearButtonSize = 'sm',
|
||||
clearButtonSx,
|
||||
containerSx,
|
||||
chipSx,
|
||||
maxVisible = 2,
|
||||
onAdd,
|
||||
onClearAll,
|
||||
onOpen,
|
||||
overflowChipSx,
|
||||
resultCount,
|
||||
resultSx,
|
||||
showAddChip = false,
|
||||
totalCount,
|
||||
}) => {
|
||||
if (!chips.length) {
|
||||
return null
|
||||
@@ -39,7 +39,7 @@ const ActiveFilterChips = ({
|
||||
...containerSx,
|
||||
}}
|
||||
>
|
||||
{visible.map(({ key, label, onClear, color = 'primary' }) => (
|
||||
{visible.map(({ color = 'primary', key, label, onClear }) => (
|
||||
<Chip
|
||||
key={key}
|
||||
size={chipSize}
|
||||
@@ -52,7 +52,10 @@ const ActiveFilterChips = ({
|
||||
<ChipDelete
|
||||
variant='plain'
|
||||
color={color}
|
||||
onDelete={() => onClear?.()}
|
||||
onDelete={event => {
|
||||
event.stopPropagation()
|
||||
onClear?.()
|
||||
}}
|
||||
aria-label={`Remove ${label} filter`}
|
||||
sx={{
|
||||
'--Chip-deleteSize': chipSize === 'sm' ? '1.1rem' : '1.4rem',
|
||||
|
||||
@@ -89,6 +89,8 @@ const buildActualDateGroups = chores => {
|
||||
export const ChoresGrouper = (groupBy, chores, filter) => {
|
||||
if (filter) {
|
||||
chores = chores.filter(chore => filter(chore))
|
||||
} else {
|
||||
chores = [...chores]
|
||||
}
|
||||
|
||||
// sort by priority then due date:
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import DurationInput from '../../components/common/DurationInput'
|
||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||
import NotificationTemplate from '../../components/NotificationTemplate.jsx'
|
||||
@@ -60,10 +61,10 @@ import {
|
||||
} from '../../utils/Fetcher'
|
||||
import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers'
|
||||
import { getImageSrc, removeCachedImage } from '../../utils/ImageCache'
|
||||
import { generateUUID } from '../../utils/UUID'
|
||||
import Priorities from '../../utils/Priorities.jsx'
|
||||
import { getIconComponent } from '../../utils/ProjectIcons'
|
||||
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
|
||||
import { generateUUID } from '../../utils/UUID'
|
||||
import { useProjectFilter } from '../Chores/hooks/useProjectFilter.js'
|
||||
import LoadingComponent from '../components/Loading.jsx'
|
||||
import RichTextEditor from '../components/RichTextEditor.jsx'
|
||||
@@ -84,6 +85,7 @@ const ASSIGN_STRATEGIES = [
|
||||
'round_robin',
|
||||
'no_assignee',
|
||||
]
|
||||
const DEFAULT_ASSIGN_STRATEGY = ASSIGN_STRATEGIES[3] // keep_last_assigned
|
||||
const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month']
|
||||
|
||||
const NO_DUE_DATE_REQUIRED_TYPE = ['no_repeat', 'once']
|
||||
@@ -103,7 +105,7 @@ const ChoreEdit = () => {
|
||||
const [anyone, setAnyone] = useState(false)
|
||||
const [assignableTo, setAssignableTo] = useState([])
|
||||
const [performers, setPerformers] = useState([])
|
||||
const [assignStrategy, setAssignStrategy] = useState(ASSIGN_STRATEGIES[2])
|
||||
const [assignStrategy, setAssignStrategy] = useState(DEFAULT_ASSIGN_STRATEGY)
|
||||
const [dueDate, setDueDate] = useState(null)
|
||||
const [dueDateOnly, setDueDateOnly] = useState(null)
|
||||
const [dueTime, setDueTime] = useState(null)
|
||||
@@ -151,7 +153,7 @@ const ChoreEdit = () => {
|
||||
const { data: userLabelsRaw, isLoading: isUserLabelsLoading } = useLabels()
|
||||
const { data: projects = [], isLoading: isProjectsLoading } = useProjects()
|
||||
|
||||
const { selectedProject, projectsWithDefault, setSelectedProjectWithCache } =
|
||||
const { projectsWithDefault, selectedProject, setSelectedProjectWithCache } =
|
||||
useProjectFilter(projects)
|
||||
|
||||
const [projectId, setProjectId] = useState(
|
||||
@@ -170,7 +172,7 @@ const ChoreEdit = () => {
|
||||
} = useChore(choreId)
|
||||
const { data: membersData, isLoading: isMemberDataLoading } =
|
||||
useCircleMembers()
|
||||
const { showSuccess, showError } = useNotification()
|
||||
const { showError, showSuccess } = useNotification()
|
||||
|
||||
const [userLabels, setUserLabels] = useState([])
|
||||
|
||||
@@ -183,20 +185,26 @@ const ChoreEdit = () => {
|
||||
const Navigate = useNavigate()
|
||||
|
||||
const assignees = anyone ? performers : assignableTo
|
||||
const hasSpecificAssignees = !anyone && assignableTo.length > 0
|
||||
const canPickStrategy = hasSpecificAssignees && assignableTo.length > 1
|
||||
const assignStrategyValue = !hasSpecificAssignees
|
||||
? 'no_assignee'
|
||||
: canPickStrategy
|
||||
? assignStrategy
|
||||
: DEFAULT_ASSIGN_STRATEGY
|
||||
const assignedToValue =
|
||||
!hasSpecificAssignees || assignStrategyValue === 'no_assignee'
|
||||
? null
|
||||
: assignableTo.some(a => a.userId === assignedTo)
|
||||
? assignedTo
|
||||
: assignableTo[0].userId
|
||||
|
||||
const HandleValidateChore = () => {
|
||||
const errors = {}
|
||||
|
||||
if (name.trim() === '') {
|
||||
errors.name = 'Name is required'
|
||||
}
|
||||
if (assignStrategy !== 'no_assignee') {
|
||||
if (assignees.length === 0) {
|
||||
errors.assignees = 'At least 1 assignees is required'
|
||||
}
|
||||
if (assignedTo === null || assignedTo < 0) {
|
||||
errors.assignedTo = 'Assigned to is required'
|
||||
}
|
||||
}
|
||||
if (frequencyType === 'interval' && !frequency > 0) {
|
||||
errors.frequency = `Invalid frequency, the ${frequencyMetadata.unit} should be > 0`
|
||||
}
|
||||
@@ -366,8 +374,8 @@ const ChoreEdit = () => {
|
||||
frequencyType: frequencyType,
|
||||
frequency: Number(frequency),
|
||||
frequencyMetadata: frequencyMetadata,
|
||||
assignedTo: assignStrategy === 'no_assignee' ? null : assignedTo,
|
||||
assignStrategy: assignStrategy,
|
||||
assignedTo: assignedToValue,
|
||||
assignStrategy: assignStrategyValue,
|
||||
isRolling: isRolling,
|
||||
isActive: isActive,
|
||||
notification: isNotificable,
|
||||
@@ -463,6 +471,20 @@ const ChoreEdit = () => {
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (choreId || !userProfile?.id) return
|
||||
|
||||
const defaultAnyoneSetting = localStorage.getItem('defaultAnyoneSetting')
|
||||
const defaultAssigneeSetting = localStorage.getItem(
|
||||
'defaultAssigneeSetting',
|
||||
)
|
||||
|
||||
if (defaultAnyoneSetting === null && defaultAssigneeSetting === null) {
|
||||
setAnyone(false)
|
||||
setAssignableTo([{ userId: userProfile.id }])
|
||||
setAssignedTo(userProfile.id)
|
||||
}
|
||||
}, [choreId, userProfile?.id])
|
||||
useEffect(() => {
|
||||
const anyoneSetting = localStorage.getItem('defaultAnyoneSetting')
|
||||
const anyoneDirty = anyoneSetting !== JSON.stringify(anyone)
|
||||
@@ -549,7 +571,7 @@ const ChoreEdit = () => {
|
||||
setAssignStrategy(
|
||||
data.res.assignStrategy
|
||||
? data.res.assignStrategy
|
||||
: ASSIGN_STRATEGIES[2],
|
||||
: DEFAULT_ASSIGN_STRATEGY,
|
||||
)
|
||||
setIsRolling(data.res.isRolling)
|
||||
setIsActive(data.res.isActive)
|
||||
@@ -632,21 +654,6 @@ const ChoreEdit = () => {
|
||||
}
|
||||
}, [frequencyType])
|
||||
|
||||
useEffect(() => {
|
||||
if (anyone || assignableTo.length === 0) {
|
||||
setAssignStrategy('no_assignee')
|
||||
setAssignedTo(null)
|
||||
} else if (assignStrategy === 'no_assignee') {
|
||||
// user explicitly picked no_assignee while having assignees, keep it
|
||||
// but there is nobody currently assigned
|
||||
if (assignedTo !== null) {
|
||||
setAssignedTo(null)
|
||||
}
|
||||
} else if (!assignableTo.some(a => a.userId === assignedTo)) {
|
||||
setAssignedTo(assignableTo[0].userId)
|
||||
}
|
||||
}, [assignStrategy, assignedTo, assignableTo, anyone])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (performers.length > 0 && assignees.length === 0 && userProfile) {
|
||||
// setAssignees([
|
||||
@@ -1260,12 +1267,13 @@ const ChoreEdit = () => {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!anyone && assignableTo.length > 1 && (
|
||||
{canPickStrategy && (
|
||||
<>
|
||||
<Box
|
||||
mb={3}
|
||||
sx={{
|
||||
display: assignStrategy === 'no_assignee' ? 'none' : 'block',
|
||||
display:
|
||||
assignStrategyValue === 'no_assignee' ? 'none' : 'block',
|
||||
}}
|
||||
>
|
||||
<Typography level='h4'>Currently Assigned To</Typography>
|
||||
@@ -1279,7 +1287,7 @@ const ChoreEdit = () => {
|
||||
: 'Select an assignee for this task'
|
||||
}
|
||||
disabled={assignees.length === 0}
|
||||
value={assignedTo > -1 ? assignedTo : null}
|
||||
value={assignedToValue}
|
||||
onChange={(_, selectedUserId) => setAssignedTo(selectedUserId)}
|
||||
>
|
||||
{performers
|
||||
@@ -1309,7 +1317,7 @@ const ChoreEdit = () => {
|
||||
{ASSIGN_STRATEGIES.map((item, idx) => (
|
||||
<ListItem key={item}>
|
||||
<Checkbox
|
||||
checked={assignStrategy === item}
|
||||
checked={assignStrategyValue === item}
|
||||
onClick={() => setAssignStrategy(item)}
|
||||
overlay
|
||||
disableIcon
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
CalendarMonth,
|
||||
CloudOff,
|
||||
EditCalendar,
|
||||
SearchOff,
|
||||
ExpandCircleDown,
|
||||
PriorityHigh,
|
||||
SearchOff,
|
||||
Style,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
@@ -20,44 +20,42 @@ import {
|
||||
IconButton,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { useChores } from '../../queries/ChoreQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import IconButtonWithMenu from './IconButtonWithMenu'
|
||||
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import EmptyState from '../../components/common/EmptyState'
|
||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||
import { useFilter } from '../../hooks/useFilter'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useFilter } from '../../hooks/useFilter'
|
||||
import { useChores } from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import {
|
||||
ChoreFilters,
|
||||
ChoresGrouper,
|
||||
ChoreSorter,
|
||||
filterByProject,
|
||||
} from '../../utils/Chores'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
import { getSafeBottom } from '../../utils/SafeAreaUtils.js'
|
||||
import TaskInput from '../components/AddTaskModal'
|
||||
import CalendarDual from '../components/CalendarDual'
|
||||
import CalendarMonthly from '../components/CalendarMonthly.jsx'
|
||||
import FeedbackPrompt from '../components/FeedbackPrompt.jsx'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import { useProjects } from '../Projects/ProjectQueries.js'
|
||||
import ChoreListView from './ChoreListView.jsx'
|
||||
import ChoreModals from './components/ChoreModals'
|
||||
import ChoreToolbar from './components/ChoreToolbarPrototype'
|
||||
import {
|
||||
conditionsToSelections,
|
||||
selectionsToConditions,
|
||||
} from './components/FilterBuilderContent'
|
||||
import ChoreModals from './components/ChoreModals'
|
||||
import MultiSelectToolbar from './components/MultiSelectToolbar'
|
||||
import MyChoreHeader from './components/MyChoreHeader'
|
||||
import { useChoreActions } from './hooks/useChoreActions'
|
||||
@@ -79,7 +77,7 @@ const MyChores = () => {
|
||||
const { data: userProfile, isLoading: isUserProfileLoading } =
|
||||
useUserProfile()
|
||||
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
||||
const { showSuccess, showError, showWarning, showUndo } = useNotification()
|
||||
const { showError, showSuccess, showUndo, showWarning } = useNotification()
|
||||
const queryClient = useQueryClient()
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
const Navigate = useNavigate()
|
||||
@@ -88,20 +86,19 @@ const MyChores = () => {
|
||||
const { data: projects = [], isLoading: projectsLoading } = useProjects()
|
||||
const {
|
||||
data: choresData,
|
||||
isLoading: choresLoading,
|
||||
isError: choresError,
|
||||
error: choresErrorDetails,
|
||||
isError: choresError,
|
||||
isLoading: choresLoading,
|
||||
refetch: refetchChores,
|
||||
} = useChores(false)
|
||||
const {
|
||||
data: membersData,
|
||||
isLoading: membersLoading,
|
||||
isError: membersError,
|
||||
isLoading: membersLoading,
|
||||
} = useCircleMembers()
|
||||
|
||||
const [chores, setChores] = useState([])
|
||||
const [filteredChores, setFilteredChores] = useState([])
|
||||
const [choreSections, setChoreSections] = useState([])
|
||||
const [addTaskModalOpen, setAddTaskModalOpen] = useState(false)
|
||||
// 'voice' | 'scan' | null — set by the quick-capture widget deep links
|
||||
const [addTaskInitialMode, setAddTaskInitialMode] = useState(null)
|
||||
@@ -118,6 +115,9 @@ const MyChores = () => {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
const openSectionsInitializedRef = useRef(
|
||||
localStorage.getItem('openChoreSections') !== null,
|
||||
)
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [viewMode, setViewMode] = useState(
|
||||
localStorage.getItem('choreCardViewMode') || 'default',
|
||||
@@ -126,15 +126,15 @@ const MyChores = () => {
|
||||
const menuRef = useRef(null)
|
||||
const [confirmModelConfig, setConfirmModelConfig] = useState({})
|
||||
|
||||
const { selectedProject, projectsWithDefault, setSelectedProjectWithCache } =
|
||||
const { projectsWithDefault, selectedProject, setSelectedProjectWithCache } =
|
||||
useProjectFilter(projects, !projectsLoading)
|
||||
|
||||
const {
|
||||
searchTerm,
|
||||
selectedChoreFilter,
|
||||
nonProjectFilteredChores,
|
||||
projectFilteredChores,
|
||||
searchFilteredChores,
|
||||
nonProjectFilteredChores,
|
||||
searchTerm,
|
||||
selectedChoreFilter,
|
||||
setSearchTerm,
|
||||
setSelectedChoreFilterWithCache,
|
||||
} = useChoreFilters({
|
||||
@@ -145,37 +145,37 @@ const MyChores = () => {
|
||||
})
|
||||
|
||||
const {
|
||||
isMultiSelectMode,
|
||||
selectedChores,
|
||||
toggleMultiSelectMode,
|
||||
toggleChoreSelection,
|
||||
enterMultiSelectWithChore,
|
||||
selectAllVisibleChores,
|
||||
clearSelection,
|
||||
enterMultiSelectWithChore,
|
||||
getSelectedChoresData,
|
||||
isMultiSelectMode,
|
||||
selectAllVisibleChores,
|
||||
selectedChores,
|
||||
toggleChoreSelection,
|
||||
toggleMultiSelectMode,
|
||||
} = useMultiSelect()
|
||||
|
||||
const { activeModal, modalChore, modalData, openModal, closeModal } =
|
||||
const { activeModal, closeModal, modalChore, modalData, openModal } =
|
||||
useChoreModals()
|
||||
|
||||
const {
|
||||
savedFilters,
|
||||
activeFilter,
|
||||
activeFilterId,
|
||||
applyCustomFilter,
|
||||
applyTempFilter,
|
||||
clearActiveFilter,
|
||||
clearTempFilter,
|
||||
createFilterFromCurrentState,
|
||||
deleteFilter,
|
||||
filteredChores: customFilteredChores,
|
||||
hasFilterApplied,
|
||||
hasProjectConditions,
|
||||
pinFilter,
|
||||
saveFilter,
|
||||
savedFilters,
|
||||
tempFilter,
|
||||
tempFilterMeta,
|
||||
filteredChores: customFilteredChores,
|
||||
applyCustomFilter,
|
||||
clearActiveFilter,
|
||||
applyTempFilter,
|
||||
clearTempFilter,
|
||||
saveFilter,
|
||||
updateFilter,
|
||||
deleteFilter,
|
||||
pinFilter,
|
||||
createFilterFromCurrentState,
|
||||
hasProjectConditions,
|
||||
hasFilterApplied,
|
||||
} = useCustomFilters(
|
||||
nonProjectFilteredChores,
|
||||
membersData?.res,
|
||||
@@ -276,10 +276,10 @@ const MyChores = () => {
|
||||
)
|
||||
|
||||
const {
|
||||
filteredData: quickFilteredChores,
|
||||
setFilter: setQuickFilter,
|
||||
clearAll: clearQuickFilters,
|
||||
filteredData: quickFilteredChores,
|
||||
hasActiveFilters: hasQuickFilters,
|
||||
setFilter: setQuickFilter,
|
||||
} = useFilter(projectFilteredChores, quickFilterDefs)
|
||||
|
||||
const processedChores = useMemo(() => {
|
||||
@@ -301,7 +301,7 @@ const MyChores = () => {
|
||||
return sortedChores
|
||||
}, [choresData?.res, impersonatedUser])
|
||||
|
||||
const processedSections = useMemo(() => {
|
||||
const choreSections = useMemo(() => {
|
||||
if (!chores.length || !userProfile?.id) {
|
||||
return []
|
||||
}
|
||||
@@ -385,26 +385,16 @@ const MyChores = () => {
|
||||
impersonatedUser?.userId,
|
||||
])
|
||||
|
||||
// Auto-update sections when processedSections changes
|
||||
useEffect(() => {
|
||||
// Always update choreSections to match processedSections, even if empty
|
||||
setChoreSections(processedSections)
|
||||
if (openSectionsInitializedRef.current || choreSections.length === 0) return
|
||||
|
||||
// Auto-open sections if needed - only check localStorage once
|
||||
if (processedSections.length > 0) {
|
||||
const storedSections = localStorage.getItem('openChoreSections')
|
||||
if (storedSections === null) {
|
||||
const openSections = processedSections.reduce(
|
||||
(acc, _section, index) => {
|
||||
acc[index] = true
|
||||
return acc
|
||||
},
|
||||
{},
|
||||
)
|
||||
setOpenChoreSections(openSections)
|
||||
}
|
||||
}
|
||||
}, [processedSections])
|
||||
openSectionsInitializedRef.current = true
|
||||
const openSections = choreSections.reduce((acc, _section, index) => {
|
||||
acc[index] = true
|
||||
return acc
|
||||
}, {})
|
||||
setOpenChoreSections(openSections)
|
||||
}, [choreSections])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||
@@ -567,17 +557,17 @@ const MyChores = () => {
|
||||
}, [tempFilterMeta?.id, searchParams])
|
||||
|
||||
const {
|
||||
handleChoreAction,
|
||||
handleChangeDueDate,
|
||||
handleCompleteWithPastDate,
|
||||
handleAssigneeChange,
|
||||
handleCompleteWithNote,
|
||||
handleNudge,
|
||||
handleBulkComplete,
|
||||
handleBulkArchive,
|
||||
handleBulkComplete,
|
||||
handleBulkDelete,
|
||||
handleBulkSkip,
|
||||
handleBulkMoveToProject,
|
||||
handleBulkSkip,
|
||||
handleChangeDueDate,
|
||||
handleChoreAction,
|
||||
handleCompleteWithNote,
|
||||
handleCompleteWithPastDate,
|
||||
handleNudge,
|
||||
} = useChoreActions({
|
||||
chores,
|
||||
filteredChores,
|
||||
@@ -603,24 +593,14 @@ const MyChores = () => {
|
||||
return customFilteredChores
|
||||
}
|
||||
|
||||
if (searchTerm?.length > 0) {
|
||||
return searchFilteredChores
|
||||
}
|
||||
|
||||
const baseChores = hasQuickFilters
|
||||
? quickFilteredChores
|
||||
: projectFilteredChores
|
||||
|
||||
if (searchTerm?.length > 0) {
|
||||
const searchableChores = baseChores.map(c => ({
|
||||
...c,
|
||||
raw_label: c.labelsV2?.map(l => l.name).join(' '),
|
||||
}))
|
||||
const fuse = new Fuse(searchableChores, {
|
||||
keys: ['name', 'raw_label'],
|
||||
includeScore: true,
|
||||
isCaseSensitive: false,
|
||||
findAllMatches: true,
|
||||
})
|
||||
return fuse.search(searchTerm).map(result => result.item)
|
||||
}
|
||||
|
||||
return baseChores
|
||||
}, [
|
||||
activeFilterId,
|
||||
@@ -629,6 +609,7 @@ const MyChores = () => {
|
||||
hasQuickFilters,
|
||||
quickFilteredChores,
|
||||
projectFilteredChores,
|
||||
searchFilteredChores,
|
||||
searchTerm,
|
||||
])
|
||||
|
||||
@@ -751,29 +732,10 @@ const MyChores = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const searchOptions = useMemo(
|
||||
() => ({
|
||||
keys: ['name', 'raw_label'],
|
||||
includeScore: true,
|
||||
isCaseSensitive: false,
|
||||
findAllMatches: true,
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
const processedChoresForSearch = useMemo(
|
||||
() =>
|
||||
chores.map(c => ({
|
||||
...c,
|
||||
raw_label: c.labelsV2?.map(l => l.name).join(' '),
|
||||
})),
|
||||
[chores],
|
||||
)
|
||||
|
||||
const fuse = useMemo(
|
||||
() => new Fuse(processedChoresForSearch, searchOptions),
|
||||
[processedChoresForSearch, searchOptions],
|
||||
)
|
||||
const clearTempFilterAndUrl = () => {
|
||||
clearTempFilter()
|
||||
updateFilterUrl(null, null)
|
||||
}
|
||||
|
||||
const handleSearchChange = e => {
|
||||
clearActiveFilter()
|
||||
@@ -790,22 +752,6 @@ const MyChores = () => {
|
||||
|
||||
const term = search.toLowerCase()
|
||||
setSearchTerm(term)
|
||||
|
||||
// Use project-filtered chores as base for search
|
||||
const baseChores = selectedProject ? projectFilteredChores : chores
|
||||
const searchableChores = baseChores.map(c => ({
|
||||
...c,
|
||||
raw_label: c.labelsV2?.map(l => l.name).join(' '),
|
||||
}))
|
||||
|
||||
const fuse = new Fuse(searchableChores, {
|
||||
keys: ['name', 'raw_label'],
|
||||
includeScore: true,
|
||||
isCaseSensitive: false,
|
||||
findAllMatches: true,
|
||||
})
|
||||
|
||||
setFilteredChores(fuse.search(term).map(result => result.item))
|
||||
// Clear selected calendar date when search changes
|
||||
setSelectedCalendarDate(null)
|
||||
}
|
||||
@@ -895,19 +841,15 @@ const MyChores = () => {
|
||||
// )
|
||||
// }
|
||||
|
||||
const getChoresForDate = useCallback(
|
||||
date => {
|
||||
const filteredChoresData = getFilteredChores
|
||||
return filteredChoresData.filter(chore => {
|
||||
if (!chore.nextDueDate) return false
|
||||
const choreDate = new Date(chore.nextDueDate).toLocaleDateString()
|
||||
const selectedDate = date.toLocaleDateString()
|
||||
return choreDate === selectedDate
|
||||
})
|
||||
},
|
||||
[getFilteredChores],
|
||||
)
|
||||
const selectedDateChores = useMemo(() => {
|
||||
if (!selectedCalendarDate) return []
|
||||
|
||||
const selectedDate = selectedCalendarDate.toLocaleDateString()
|
||||
return getFilteredChores.filter(chore => {
|
||||
if (!chore.nextDueDate) return false
|
||||
return new Date(chore.nextDueDate).toLocaleDateString() === selectedDate
|
||||
})
|
||||
}, [getFilteredChores, selectedCalendarDate])
|
||||
|
||||
// "Narrowed" means the user actively cut the list down (search, quick
|
||||
// filters, a saved filter). Picking a project is not narrowing: an empty
|
||||
@@ -929,7 +871,6 @@ const MyChores = () => {
|
||||
const appendChore = (prev, newChore) => {
|
||||
let newChores = [...prev, newChore]
|
||||
|
||||
|
||||
if (impersonatedUser) {
|
||||
newChores = newChores.filter(
|
||||
chore => chore.assignedTo === impersonatedUser.userId,
|
||||
@@ -1010,7 +951,7 @@ const MyChores = () => {
|
||||
tempFilter={tempFilter}
|
||||
tempFilterMeta={tempFilterMeta}
|
||||
applyTempFilter={applyTempFilter}
|
||||
clearTempFilter={clearTempFilter}
|
||||
clearTempFilter={clearTempFilterAndUrl}
|
||||
saveFilter={saveFilter}
|
||||
updateFilter={updateFilter}
|
||||
onFilterSaved={name =>
|
||||
@@ -1363,7 +1304,7 @@ const MyChores = () => {
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{getChoresForDate(selectedCalendarDate).length === 0 ? (
|
||||
{selectedDateChores.length === 0 ? (
|
||||
<EmptyState
|
||||
size='sm'
|
||||
icon={<EditCalendar />}
|
||||
@@ -1377,7 +1318,7 @@ const MyChores = () => {
|
||||
/>
|
||||
) : (
|
||||
<ChoreListView
|
||||
chores={getChoresForDate(selectedCalendarDate)}
|
||||
chores={selectedDateChores}
|
||||
viewMode={'compact'}
|
||||
membersData={membersData}
|
||||
userLabels={userLabels}
|
||||
@@ -1457,18 +1398,20 @@ const MyChores = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ChoreListView
|
||||
chores={section.content}
|
||||
viewMode={viewMode}
|
||||
membersData={membersData}
|
||||
userLabels={userLabels}
|
||||
handleLabelFiltering={handleLabelFiltering}
|
||||
handleChoreAction={handleChoreAction}
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
selectedChores={selectedChores}
|
||||
toggleChoreSelection={toggleChoreSelection}
|
||||
onLongPressChore={enterMultiSelectWithChore}
|
||||
/>
|
||||
{openChoreSections[index] && (
|
||||
<ChoreListView
|
||||
chores={section.content}
|
||||
viewMode={viewMode}
|
||||
membersData={membersData}
|
||||
userLabels={userLabels}
|
||||
handleLabelFiltering={handleLabelFiltering}
|
||||
handleChoreAction={handleChoreAction}
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
selectedChores={selectedChores}
|
||||
toggleChoreSelection={toggleChoreSelection}
|
||||
onLongPressChore={enterMultiSelectWithChore}
|
||||
/>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
@@ -1574,7 +1517,7 @@ const MyChores = () => {
|
||||
allChores={chores}
|
||||
performers={membersData?.res || []}
|
||||
applyTempFilter={applyTempFilter}
|
||||
clearTempFilter={clearTempFilter}
|
||||
clearTempFilter={clearTempFilterAndUrl}
|
||||
tempFilter={tempFilter}
|
||||
/>
|
||||
|
||||
|
||||
@@ -22,23 +22,36 @@ export const useChoreFilters = ({
|
||||
return filterByProject(chores, selectedProject.id)
|
||||
}, [chores, selectedProject])
|
||||
|
||||
const hasSearchTerm = searchTerm.length > 0
|
||||
|
||||
const searchIndex = useMemo(() => {
|
||||
if (!hasSearchTerm) return null
|
||||
|
||||
const searchableChores = chores.map(chore => ({
|
||||
...chore,
|
||||
raw_label: chore.labelsV2?.map(label => label.name).join(' '),
|
||||
}))
|
||||
return new Fuse(searchableChores, {
|
||||
keys: ['name', 'raw_label'],
|
||||
includeScore: true,
|
||||
isCaseSensitive: false,
|
||||
findAllMatches: true,
|
||||
})
|
||||
}, [chores, hasSearchTerm])
|
||||
|
||||
const projectChoreIds = useMemo(
|
||||
() => new Set(projectFilteredChores.map(chore => chore.id)),
|
||||
[projectFilteredChores],
|
||||
)
|
||||
|
||||
const searchFilteredChores = useMemo(() => {
|
||||
let baseChores = projectFilteredChores
|
||||
|
||||
if (searchTerm?.length > 0) {
|
||||
const searchableChores = baseChores.map(c => ({
|
||||
...c,
|
||||
raw_label: c.labelsV2?.map(l => l.name).join(' '),
|
||||
}))
|
||||
|
||||
const fuse = new Fuse(searchableChores, {
|
||||
keys: ['name', 'raw_label'],
|
||||
includeScore: true,
|
||||
isCaseSensitive: false,
|
||||
findAllMatches: true,
|
||||
})
|
||||
|
||||
return fuse.search(searchTerm.toLowerCase()).map(result => result.item)
|
||||
if (searchIndex) {
|
||||
return searchIndex
|
||||
.search(searchTerm.toLowerCase())
|
||||
.map(result => result.item)
|
||||
.filter(chore => projectChoreIds.has(chore.id))
|
||||
}
|
||||
|
||||
if (impersonatedUser) {
|
||||
@@ -55,6 +68,8 @@ export const useChoreFilters = ({
|
||||
}, [
|
||||
searchTerm,
|
||||
projectFilteredChores,
|
||||
searchIndex,
|
||||
projectChoreIds,
|
||||
impersonatedUser,
|
||||
userProfile?.id,
|
||||
selectedChoreFilter,
|
||||
@@ -64,20 +79,10 @@ export const useChoreFilters = ({
|
||||
const nonProjectFilteredChores = useMemo(() => {
|
||||
let baseChores = chores
|
||||
|
||||
if (searchTerm?.length > 0) {
|
||||
const searchableChores = baseChores.map(c => ({
|
||||
...c,
|
||||
raw_label: c.labelsV2?.map(l => l.name).join(' '),
|
||||
}))
|
||||
|
||||
const fuse = new Fuse(searchableChores, {
|
||||
keys: ['name', 'raw_label'],
|
||||
includeScore: true,
|
||||
isCaseSensitive: false,
|
||||
findAllMatches: true,
|
||||
})
|
||||
|
||||
return fuse.search(searchTerm.toLowerCase()).map(result => result.item)
|
||||
if (searchIndex) {
|
||||
return searchIndex
|
||||
.search(searchTerm.toLowerCase())
|
||||
.map(result => result.item)
|
||||
}
|
||||
|
||||
if (impersonatedUser) {
|
||||
@@ -94,6 +99,7 @@ export const useChoreFilters = ({
|
||||
}, [
|
||||
searchTerm,
|
||||
chores,
|
||||
searchIndex,
|
||||
impersonatedUser,
|
||||
userProfile?.id,
|
||||
selectedChoreFilter,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { useUserProfile } from '../../../queries/UserQueries'
|
||||
import {
|
||||
applyFilter,
|
||||
getFilterCount,
|
||||
getFilterOverdueCount,
|
||||
validateFilter,
|
||||
} from '../../../utils/FilterEngine'
|
||||
import { applyFilter, validateFilter } from '../../../utils/FilterEngine'
|
||||
import {
|
||||
useCreateFilter,
|
||||
useDeleteFilter,
|
||||
@@ -43,12 +39,17 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
|
||||
|
||||
return filtersData.map(filter => {
|
||||
const validation = validateFilter(filter, context)
|
||||
const count = validation.isValid
|
||||
? getFilterCount(chores, filter, context)
|
||||
: 0
|
||||
const overdueCount = validation.isValid
|
||||
? getFilterOverdueCount(chores, filter, context)
|
||||
: 0
|
||||
const matchingChores = validation.isValid
|
||||
? applyFilter(chores, filter, context)
|
||||
: []
|
||||
const count = matchingChores.length
|
||||
const now = new Date()
|
||||
const overdueCount = matchingChores.reduce((total, chore) => {
|
||||
if (!chore.nextDueDate || new Date(chore.nextDueDate) >= now) {
|
||||
return total
|
||||
}
|
||||
return total + 1
|
||||
}, 0)
|
||||
|
||||
const result = {
|
||||
...filter,
|
||||
|
||||
Reference in New Issue
Block a user