migrate filter management to API and implement query hooks for filters

This commit is contained in:
Mo Tarbin
2026-02-04 00:06:44 -05:00
parent d666f59c1b
commit 6c95c683ba
6 changed files with 578 additions and 429 deletions

View File

@@ -1,220 +0,0 @@
/**
* Custom Filter Storage - Manages saving/loading custom filters
*
* This module handles CRUD operations for custom filters.
* Currently uses localStorage, but designed to easily migrate to backend API.
*/
const STORAGE_KEY = 'customFilters'
const MAX_FILTERS = 20 // Limit to prevent localStorage overflow
const generateFilterId = () => {
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
export const getSavedFilters = () => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (!stored) return []
const filters = JSON.parse(stored)
return filters.filter(f => f.id && f.name && f.conditions)
} catch (error) {
console.error('Error loading saved filters:', error)
return []
}
}
export const saveFilter = filter => {
try {
const filters = getSavedFilters()
// Check limit
if (filters.length >= MAX_FILTERS) {
throw new Error(
`Maximum of ${MAX_FILTERS} filters allowed. Please delete some filters first.`,
)
}
// Create new filter with metadata
const newFilter = {
id: generateFilterId(),
name: filter.name,
description: filter.description || '',
color: filter.color || null,
icon: filter.icon || null,
conditions: filter.conditions,
operator: filter.operator || 'AND',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
isPinned: filter.isPinned || false,
usageCount: 0,
lastUsedAt: null,
}
// Add to filters array
const updatedFilters = [...filters, newFilter]
// Save to localStorage
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedFilters))
return newFilter
} catch (error) {
console.error('Error saving filter:', error)
throw error
}
}
export const updateFilter = (filterId, updates) => {
try {
const filters = getSavedFilters()
const filterIndex = filters.findIndex(f => f.id === filterId)
if (filterIndex === -1) {
throw new Error('Filter not found')
}
// Update filter
const updatedFilter = {
...filters[filterIndex],
...updates,
updatedAt: new Date().toISOString(),
}
filters[filterIndex] = updatedFilter
// Save to localStorage
localStorage.setItem(STORAGE_KEY, JSON.stringify(filters))
return updatedFilter
} catch (error) {
console.error('Error updating filter:', error)
throw error
}
}
export const deleteFilter = filterId => {
try {
const filters = getSavedFilters()
const updatedFilters = filters.filter(f => f.id !== filterId)
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedFilters))
return true
} catch (error) {
console.error('Error deleting filter:', error)
throw error
}
}
export const getFilterById = filterId => {
const filters = getSavedFilters()
return filters.find(f => f.id === filterId) || null
}
export const trackFilterUsage = filterId => {
try {
const filters = getSavedFilters()
const filterIndex = filters.findIndex(f => f.id === filterId)
if (filterIndex !== -1) {
filters[filterIndex].usageCount =
(filters[filterIndex].usageCount || 0) + 1
filters[filterIndex].lastUsedAt = new Date().toISOString()
localStorage.setItem(STORAGE_KEY, JSON.stringify(filters))
}
} catch (error) {
console.error('Error tracking filter usage:', error)
}
}
export const toggleFilterPin = filterId => {
try {
const filters = getSavedFilters()
const filterIndex = filters.findIndex(f => f.id === filterId)
if (filterIndex === -1) {
throw new Error('Filter not found')
}
filters[filterIndex].isPinned = !filters[filterIndex].isPinned
filters[filterIndex].updatedAt = new Date().toISOString()
localStorage.setItem(STORAGE_KEY, JSON.stringify(filters))
return filters[filterIndex].isPinned
} catch (error) {
console.error('Error toggling filter pin:', error)
throw error
}
}
export const getFiltersByUsage = () => {
const filters = getSavedFilters()
return filters.sort((a, b) => (b.usageCount || 0) - (a.usageCount || 0))
}
export const getPinnedFilters = () => {
const filters = getSavedFilters()
return filters.filter(f => f.isPinned)
}
export const filterNameExists = (name, excludeId = null) => {
const filters = getSavedFilters()
return filters.some(
f => f.name.toLowerCase() === name.toLowerCase() && f.id !== excludeId,
)
}
export const exportFilters = () => {
const filters = getSavedFilters()
return JSON.stringify(filters, null, 2)
}
export const importFilters = jsonString => {
try {
const importedFilters = JSON.parse(jsonString)
if (!Array.isArray(importedFilters)) {
throw new Error('Invalid filter format')
}
const existingFilters = getSavedFilters()
// Generate new IDs for imported filters to avoid conflicts
const newFilters = importedFilters.map(filter => ({
...filter,
id: generateFilterId(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
usageCount: 0,
lastUsedAt: null,
}))
const allFilters = [...existingFilters, ...newFilters]
// Check limit
if (allFilters.length > MAX_FILTERS) {
throw new Error(`Import would exceed maximum of ${MAX_FILTERS} filters`)
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(allFilters))
return newFilters.length
} catch (error) {
console.error('Error importing filters:', error)
throw error
}
}
export const clearAllFilters = () => {
try {
localStorage.removeItem(STORAGE_KEY)
return true
} catch (error) {
console.error('Error clearing filters:', error)
throw error
}
}

View File

@@ -839,6 +839,72 @@ const DeleteProject = id => {
})
}
// Filter-related API functions
const GetFilters = () => {
return Fetch(`/filters`, {
method: 'GET',
headers: HEADERS(),
})
}
const GetPinnedFilters = () => {
return Fetch(`/filters/pinned`, {
method: 'GET',
headers: HEADERS(),
})
}
const GetFiltersByUsage = () => {
return Fetch(`/filters/by-usage`, {
method: 'GET',
headers: HEADERS(),
})
}
const GetFilterById = id => {
return Fetch(`/filters/${id}`, {
method: 'GET',
headers: HEADERS(),
})
}
const CreateFilter = filter => {
return Fetch(`/filters`, {
method: 'POST',
headers: HEADERS(),
body: JSON.stringify(filter),
})
}
const UpdateFilter = (id, filter) => {
return Fetch(`/filters/${id}`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify(filter),
})
}
const DeleteFilter = id => {
return Fetch(`/filters/${id}`, {
method: 'DELETE',
headers: HEADERS(),
})
}
const ToggleFilterPin = id => {
return Fetch(`/filters/${id}/toggle-pin`, {
method: 'POST',
headers: HEADERS(),
})
}
const TrackFilterUsage = id => {
return Fetch(`/filters/${id}/track-usage`, {
method: 'POST',
headers: HEADERS(),
})
}
export {
AcceptCircleMemberRequest,
ApproveChore,
@@ -853,6 +919,7 @@ export {
CreateChildUser,
CreateChore,
createChore,
CreateFilter,
CreateLabel,
CreateLongLiveToken,
CreateProject,
@@ -861,6 +928,7 @@ export {
DeleteChore,
DeleteChoreHistory,
DeleteCircleMember,
DeleteFilter,
DeleteLabel,
DeleteLongLiveToken,
DeleteProject,
@@ -881,9 +949,13 @@ export {
GetChoreTimer,
GetCircleMemberRequests,
GetDeviceTokens,
GetFilterById,
GetFilters,
GetFiltersByUsage,
GetLabels,
GetLongLiveTokens,
GetMFAStatus,
GetPinnedFilters,
GetProjectById,
GetProjects,
GetResource,
@@ -916,6 +988,8 @@ export {
signUp,
SkipChore,
StartChore,
ToggleFilterPin,
TrackFilterUsage,
UnArchiveChore,
UndoChoreAction,
UnregisterDeviceToken,
@@ -924,6 +998,7 @@ export {
UpdateChoreHistory,
UpdateChorePriority,
UpdateDueDate,
UpdateFilter,
UpdateLabel,
UpdateMemberRole,
UpdateNotificationTarget,

View File

@@ -63,6 +63,7 @@ const CustomFilterChips = ({
if (!a.isPinned && b.isPinned) return 1
return (b.usageCount || 0) - (a.usageCount || 0)
})
if (sortedFilters.filter(f => f.isPinned).length === 0) return null
return (
<Box
@@ -78,118 +79,115 @@ const CustomFilterChips = ({
msOverflowStyle: 'none', // IE and Edge
}}
>
{sortedFilters
.filter(f => f.isPinned)
.map(filter => {
const isActive = activeFilterId === filter.id
const hasWarning = !filter.isValid
const hasCustomColor = !!filter.color && !hasWarning
const textColor = hasCustomColor
? getTextColorFromBackgroundColor(filter.color)
: undefined
{sortedFilters.map(filter => {
const isActive = activeFilterId === filter.id
const hasWarning = !filter.isValid
const hasCustomColor = !!filter.color && !hasWarning
const textColor = hasCustomColor
? getTextColorFromBackgroundColor(filter.color)
: undefined
return (
<Tooltip
key={filter.id}
title={
hasWarning
? `Filter has issues: ${filter.validationIssues?.join(', ')}`
: `${filter.description ? filter.description + ' - ' : ''}${filter.count} tasks${filter.overdueCount > 0 ? ` (${filter.overdueCount} overdue)` : ''}`
}
placement='bottom'
>
<div onClick={() => !hasWarning && onFilterClick(filter.id)}>
<Chip
variant='solid'
size='md'
onContextMenu={e => handleContextMenu(e, filter)}
sx={{
cursor: hasWarning ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
px: 1.0,
return (
<Tooltip
key={filter.id}
title={
hasWarning
? `Filter has issues: ${filter.validationIssues?.join(', ')}`
: `${filter.description ? filter.description + ' - ' : ''}${filter.count} tasks${filter.overdueCount > 0 ? ` (${filter.overdueCount} overdue)` : ''}`
}
placement='bottom'
>
<div onClick={() => !hasWarning && onFilterClick(filter.id)}>
<Chip
variant='solid'
size='md'
onContextMenu={e => handleContextMenu(e, filter)}
sx={{
cursor: hasWarning ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
px: 1.0,
py: 0.5,
opacity: hasWarning ? 0.7 : isActive ? 1 : 0.85,
...(hasCustomColor && {
opacity: hasWarning ? 0.7 : isActive ? 1 : 0.85,
...(hasCustomColor && {
backgroundColor: `${filter.color} !important`,
color: `${textColor} !important`,
'&:hover': {
backgroundColor: `${filter.color} !important`,
color: `${textColor} !important`,
'&:hover': {
backgroundColor: `${filter.color} !important`,
filter: 'brightness(0.95)',
opacity: 1,
},
filter: 'brightness(0.95)',
opacity: 1,
},
}),
}}
startDecorator={
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
{isActive ? (
<Check
sx={{
fontSize: '1rem',
color: hasCustomColor ? textColor : 'primary.500',
}}
/>
) : (
<Chip
size='sm'
variant='solid'
sx={{
...(hasCustomColor
? {
bgcolor:
textColor === '#FFFFFF'
? '#00000040'
: '#FFFFFF40',
color: textColor,
border: `1px solid ${textColor}30`,
}
: {}),
}}
color={
hasCustomColor
? undefined
: hasWarning
? 'warning'
: 'neutral'
}
>
{filter.count}
</Chip>
)}
</Box>
}
// endDecorator={
// <Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
// {hasWarning && <Warning sx={{ fontSize: '1rem' }} />}
// {!hasWarning && filter.overdueCount > 0 && (
// <Chip size='sm' color='danger' variant='solid'>
// {filter.overdueCount}
// </Chip>
// )}
// </Box>
// }
>
<Typography
level='body-sm'
fontWeight={isActive ? 'md' : 'normal'}
sx={{
whiteSpace: 'nowrap',
maxWidth: 100,
overflow: 'hidden',
textOverflow: 'ellipsis',
...(hasCustomColor && {
color: textColor,
}),
}}
startDecorator={
<Box
sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}
>
{isActive ? (
<Check
sx={{
fontSize: '1rem',
color: hasCustomColor ? textColor : 'primary.500',
}}
/>
) : (
<Chip
size='sm'
variant='solid'
sx={{
...(hasCustomColor
? {
bgcolor:
textColor === '#FFFFFF'
? '#00000040'
: '#FFFFFF40',
color: textColor,
border: `1px solid ${textColor}30`,
}
: {}),
}}
color={
hasCustomColor
? undefined
: hasWarning
? 'warning'
: 'neutral'
}
>
{filter.count}
</Chip>
)}
</Box>
}
// endDecorator={
// <Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
// {hasWarning && <Warning sx={{ fontSize: '1rem' }} />}
// {!hasWarning && filter.overdueCount > 0 && (
// <Chip size='sm' color='danger' variant='solid'>
// {filter.overdueCount}
// </Chip>
// )}
// </Box>
// }
>
<Typography
level='body-sm'
fontWeight={isActive ? 'md' : 'normal'}
sx={{
whiteSpace: 'nowrap',
maxWidth: 100,
overflow: 'hidden',
textOverflow: 'ellipsis',
...(hasCustomColor && {
color: textColor,
}),
}}
>
{filter.name}
</Typography>
</Chip>
</div>
</Tooltip>
)
})}
{filter.name}
</Typography>
</Chip>
</div>
</Tooltip>
)
})}
<Chip
variant='outlined'

View File

@@ -1,35 +1,34 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useUserProfile } from '../../../queries/UserQueries'
import {
deleteFilter as deleteFilterStorage,
getSavedFilters,
saveFilter as saveFilterStorage,
toggleFilterPin,
trackFilterUsage,
updateFilter as updateFilterStorage,
} from '../../../utils/CustomFilterStorage'
import {
applyFilter,
getFilterCount,
getFilterOverdueCount,
validateFilter,
} from '../../../utils/FilterEngine'
import {
useFilters,
useCreateFilter,
useUpdateFilter,
useDeleteFilter,
useToggleFilterPin,
useTrackFilterUsage,
} from '../../Filters/FilterQueries'
export const useCustomFilters = (chores, membersData, labels, projects) => {
const { data: userProfile } = useUserProfile()
const [savedFilters, setSavedFilters] = useState([])
// React Query hooks
const { data: filtersData = [] } = useFilters()
const createFilterMutation = useCreateFilter()
const updateFilterMutation = useUpdateFilter()
const deleteFilterMutation = useDeleteFilter()
const togglePinMutation = useToggleFilterPin()
const trackUsageMutation = useTrackFilterUsage()
const [activeFilterId, setActiveFilterId] = useState(null)
const [tempFilter, setTempFilter] = useState(null)
const loadFilters = useCallback(() => {
const filters = getSavedFilters()
setSavedFilters(filters)
}, [])
useEffect(() => {
loadFilters()
}, [loadFilters])
const context = useMemo(
() => ({
userId: userProfile?.id,
@@ -43,7 +42,7 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
const filtersWithCounts = useMemo(() => {
if (!chores || !Array.isArray(chores)) return []
return savedFilters.map(filter => {
return filtersData.map(filter => {
const validation = validateFilter(filter, context)
const count = validation.isValid
? getFilterCount(chores, filter, context)
@@ -62,7 +61,7 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
return result
})
}, [savedFilters, chores, context])
}, [filtersData, chores, context])
const activeFilter = useMemo(() => {
if (!activeFilterId) return null
@@ -94,10 +93,9 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
const applyCustomFilter = useCallback(
filterId => {
setActiveFilterId(filterId)
trackFilterUsage(filterId)
loadFilters()
trackUsageMutation.mutate(filterId)
},
[loadFilters],
[trackUsageMutation],
)
const clearActiveFilter = useCallback(() => {
@@ -116,20 +114,37 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
const saveFilter = useCallback(
filter => {
const savedFilter = saveFilterStorage(filter)
loadFilters()
return savedFilter
return new Promise((resolve, reject) => {
createFilterMutation.mutate(filter, {
onSuccess: savedFilter => {
resolve(savedFilter)
},
onError: error => {
reject(error)
},
})
})
},
[loadFilters],
[createFilterMutation],
)
const updateFilter = useCallback(
(filterId, updates) => {
const updated = updateFilterStorage(filterId, updates)
loadFilters()
return updated
return new Promise((resolve, reject) => {
updateFilterMutation.mutate(
{ filterId, filterData: updates },
{
onSuccess: updated => {
resolve(updated)
},
onError: error => {
reject(error)
},
},
)
})
},
[loadFilters],
[updateFilterMutation],
)
const deleteFilter = useCallback(
@@ -137,18 +152,16 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
if (activeFilterId === filterId) {
setActiveFilterId(null)
}
deleteFilterStorage(filterId)
loadFilters()
deleteFilterMutation.mutate(filterId)
},
[activeFilterId, loadFilters],
[activeFilterId, deleteFilterMutation],
)
const pinFilter = useCallback(
filterId => {
toggleFilterPin(filterId)
loadFilters()
togglePinMutation.mutate(filterId)
},
[loadFilters],
[togglePinMutation],
)
const createFilterFromCurrentState = useCallback(

View File

@@ -0,0 +1,294 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
GetFilters,
GetFilterById,
GetPinnedFilters,
GetFiltersByUsage,
CreateFilter,
UpdateFilter,
DeleteFilter,
ToggleFilterPin,
TrackFilterUsage,
} from '../../utils/Fetcher'
// Query hook for fetching all filters
export const useFilters = () => {
return useQuery({
queryKey: ['filters'],
queryFn: async () => {
try {
const response = await GetFilters()
if (response.ok) {
const data = await response.json()
return data.res || data
}
throw new Error('Failed to fetch filters')
} catch (error) {
console.error('Error fetching filters:', error)
return []
}
},
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
})
}
// Query hook for fetching pinned filters
export const usePinnedFilters = () => {
return useQuery({
queryKey: ['filters', 'pinned'],
queryFn: async () => {
try {
const response = await GetPinnedFilters()
if (response.ok) {
const data = await response.json()
return data.res || data
}
throw new Error('Failed to fetch pinned filters')
} catch (error) {
console.error('Error fetching pinned filters:', error)
return []
}
},
staleTime: 5 * 60 * 1000,
cacheTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
})
}
// Query hook for fetching filters by usage
export const useFiltersByUsage = () => {
return useQuery({
queryKey: ['filters', 'by-usage'],
queryFn: async () => {
try {
const response = await GetFiltersByUsage()
if (response.ok) {
const data = await response.json()
return data.res || data
}
throw new Error('Failed to fetch filters by usage')
} catch (error) {
console.error('Error fetching filters by usage:', error)
return []
}
},
staleTime: 5 * 60 * 1000,
cacheTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
})
}
// Hook to get a specific filter by ID
export const useFilter = filterId => {
return useQuery({
queryKey: ['filters', filterId],
queryFn: async () => {
try {
const response = await GetFilterById(filterId)
if (response.ok) {
const data = await response.json()
return data.res || data
}
throw new Error('Failed to fetch filter')
} catch (error) {
console.error('Error fetching filter:', error)
return null
}
},
enabled: !!filterId,
staleTime: 5 * 60 * 1000,
cacheTime: 10 * 60 * 1000,
})
}
// Mutation hook for creating a new filter
export const useCreateFilter = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async filterData => {
try {
const response = await CreateFilter(filterData)
if (response.ok) {
const data = await response.json()
return data.res || data
}
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to create filter')
} catch (error) {
console.error('Error creating filter:', error)
throw error
}
},
onSuccess: newFilter => {
// Update the filters cache
queryClient.setQueryData(['filters'], oldFilters => {
if (!oldFilters) return [newFilter]
return [...oldFilters, newFilter]
})
// Invalidate and refetch
queryClient.invalidateQueries(['filters'])
},
onError: error => {
console.error('Create filter mutation failed:', error)
},
})
}
// Mutation hook for updating an existing filter
export const useUpdateFilter = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ filterId, filterData }) => {
try {
const response = await UpdateFilter(filterId, filterData)
if (response.ok) {
const data = await response.json()
return data.res || data
}
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to update filter')
} catch (error) {
console.error('Error updating filter:', error)
throw error
}
},
onSuccess: updatedFilter => {
// Update the filters cache
queryClient.setQueryData(['filters'], oldFilters => {
if (!oldFilters) return [updatedFilter]
return oldFilters.map(filter =>
filter.id === updatedFilter.id ? updatedFilter : filter,
)
})
// Update specific filter cache
queryClient.setQueryData(['filters', updatedFilter.id], updatedFilter)
// Invalidate and refetch
queryClient.invalidateQueries(['filters'])
},
onError: error => {
console.error('Update filter mutation failed:', error)
},
})
}
// Mutation hook for deleting a filter
export const useDeleteFilter = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async filterId => {
try {
const response = await DeleteFilter(filterId)
if (response.ok) {
return { id: filterId, deleted: true }
}
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to delete filter')
} catch (error) {
console.error('Error deleting filter:', error)
throw error
}
},
onSuccess: ({ id: deletedFilterId }) => {
// Remove the filter from cache
queryClient.setQueryData(['filters'], oldFilters => {
if (!oldFilters) return []
return oldFilters.filter(filter => filter.id !== deletedFilterId)
})
// Invalidate and refetch
queryClient.invalidateQueries(['filters'])
},
onError: error => {
console.error('Delete filter mutation failed:', error)
},
})
}
// Mutation hook for toggling filter pin status
export const useToggleFilterPin = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async filterId => {
try {
const response = await ToggleFilterPin(filterId)
if (response.ok) {
const data = await response.json()
return data.res || data
}
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to toggle filter pin')
} catch (error) {
console.error('Error toggling filter pin:', error)
throw error
}
},
onSuccess: updatedFilter => {
// Update the filters cache
queryClient.setQueryData(['filters'], oldFilters => {
if (!oldFilters) return [updatedFilter]
return oldFilters.map(filter =>
filter.id === updatedFilter.id ? updatedFilter : filter,
)
})
// Update specific filter cache
queryClient.setQueryData(['filters', updatedFilter.id], updatedFilter)
// Invalidate related queries
queryClient.invalidateQueries(['filters'])
queryClient.invalidateQueries(['filters', 'pinned'])
},
onError: error => {
console.error('Toggle filter pin mutation failed:', error)
},
})
}
// Mutation hook for tracking filter usage
export const useTrackFilterUsage = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async filterId => {
try {
const response = await TrackFilterUsage(filterId)
if (response.ok) {
const data = await response.json()
return data.res || data
}
// Silently fail for tracking - not critical
return null
} catch (error) {
console.error('Error tracking filter usage:', error)
// Silently fail for tracking
return null
}
},
onSuccess: updatedFilter => {
if (!updatedFilter) return
// Update the filters cache
queryClient.setQueryData(['filters'], oldFilters => {
if (!oldFilters) return [updatedFilter]
return oldFilters.map(filter =>
filter.id === updatedFilter.id ? updatedFilter : filter,
)
})
// Update specific filter cache
queryClient.setQueryData(['filters', updatedFilter.id], updatedFilter)
// Invalidate usage-related queries
queryClient.invalidateQueries(['filters', 'by-usage'])
},
})
}

View File

@@ -23,13 +23,6 @@ import {
} from '@mui/icons-material'
import { useChores } from '../../queries/ChoreQueries'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import {
deleteFilter,
getSavedFilters,
saveFilter,
toggleFilterPin,
updateFilter,
} from '../../utils/CustomFilterStorage'
import { getFilterCount, getFilterOverdueCount } from '../../utils/FilterEngine'
import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
@@ -37,6 +30,13 @@ import { useLabels } from '../Labels/LabelQueries'
import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import { useProjects } from '../Projects/ProjectQueries'
import {
useFilters,
useCreateFilter,
useUpdateFilter,
useDeleteFilter,
useToggleFilterPin,
} from './FilterQueries'
const FilterCard = ({
filter,
@@ -551,42 +551,32 @@ const FilterView = () => {
const { data: projects = [] } = useProjects()
const { data: membersData } = useCircleMembers()
const [savedFilters, setSavedFilters] = useState([])
// React Query hooks
const { data: filtersData = [], isLoading } = useFilters()
const createFilterMutation = useCreateFilter()
const updateFilterMutation = useUpdateFilter()
const deleteFilterMutation = useDeleteFilter()
const togglePinMutation = useToggleFilterPin()
const [filterCounts, setFilterCounts] = useState({})
const [showAdvancedFilterBuilder, setShowAdvancedFilterBuilder] =
useState(false)
const [editingFilter, setEditingFilter] = useState(null)
const [confirmationModel, setConfirmationModel] = useState({})
const [isLoading, setIsLoading] = useState(true)
// Load filters
const loadFilters = () => {
try {
const filters = getSavedFilters()
// Sort: pinned first, then by usage count, then by last used
const sortedFilters = filters.sort((a, b) => {
if (a.isPinned !== b.isPinned) {
return a.isPinned ? -1 : 1
}
if ((b.usageCount || 0) !== (a.usageCount || 0)) {
return (b.usageCount || 0) - (a.usageCount || 0)
}
if (a.lastUsedAt && b.lastUsedAt) {
return new Date(b.lastUsedAt) - new Date(a.lastUsedAt)
}
return new Date(b.createdAt) - new Date(a.createdAt)
})
setSavedFilters(sortedFilters)
} catch (error) {
console.error('Error loading filters:', error)
} finally {
setIsLoading(false)
// Sort filters: pinned first, then by usage count, then by last used
const savedFilters = [...filtersData].sort((a, b) => {
if (a.isPinned !== b.isPinned) {
return a.isPinned ? -1 : 1
}
}
useEffect(() => {
loadFilters()
}, [])
if ((b.usageCount || 0) !== (a.usageCount || 0)) {
return (b.usageCount || 0) - (a.usageCount || 0)
}
if (a.lastUsedAt && b.lastUsedAt) {
return new Date(b.lastUsedAt) - new Date(a.lastUsedAt)
}
return new Date(b.createdAt) - new Date(a.createdAt)
})
// Calculate task counts for each filter
useEffect(() => {
@@ -659,41 +649,40 @@ const FilterView = () => {
}
const handleDeleteFilter = id => {
try {
deleteFilter(id)
// if it's the selected filter, we might want to clear it
localStorage.getItem('selectedChoreFilter') === id &&
localStorage.removeItem('selectedChoreFilter')
loadFilters()
} catch (error) {
console.error('Error deleting filter:', error)
}
deleteFilterMutation.mutate(id, {
onSuccess: () => {
// if it's the selected filter, we might want to clear it
if (localStorage.getItem('selectedChoreFilter') === id) {
localStorage.removeItem('selectedChoreFilter')
}
},
})
}
const handlePinFilter = id => {
try {
toggleFilterPin(id)
loadFilters()
} catch (error) {
console.error('Error pinning filter:', error)
}
togglePinMutation.mutate(id)
}
const handleSaveFilter = filterData => {
try {
if (editingFilter) {
// Update existing filter
updateFilter(editingFilter.id, filterData)
} else {
// Save new filter
saveFilter(filterData)
}
setShowAdvancedFilterBuilder(false)
setEditingFilter(null)
loadFilters()
} catch (error) {
console.error('Error saving filter:', error)
if (editingFilter) {
// Update existing filter
updateFilterMutation.mutate(
{ filterId: editingFilter.id, filterData },
{
onSuccess: () => {
setShowAdvancedFilterBuilder(false)
setEditingFilter(null)
},
},
)
} else {
// Save new filter
createFilterMutation.mutate(filterData, {
onSuccess: () => {
setShowAdvancedFilterBuilder(false)
setEditingFilter(null)
},
})
}
}