migrate filter management to API and implement query hooks for filters
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(
|
||||
|
||||
294
src/views/Filters/FilterQueries.js
Normal file
294
src/views/Filters/FilterQueries.js
Normal 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'])
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user