From ad45bafd6229511e1980fdca4e8211507386495d Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Mon, 26 Jan 2026 17:27:27 -0500 Subject: [PATCH] feat: Refactor Filter Section and Add Filter View - Removed unused props and components from FilterSection. - Introduced FilterView component to manage and display saved filters. - Implemented swipe functionality for filter actions (edit, delete, pin). - Enhanced AdvancedFilterBuilder with description and color selection. - Deleted obsolete SaveFilterModal component. - Updated NavBar to include a link to the new Filters page. - Adjusted chore filter hooks for improved functionality and organization. --- src/contexts/RouterContext.jsx | 5 + src/utils/Colors.jsx | 4 + src/utils/CustomFilterStorage.js | 2 + src/views/Chores/MyChores.jsx | 45 +- src/views/Chores/SortAndGrouping.jsx | 17 + .../Chores/components/CustomFilterChips.jsx | 180 ++-- src/views/Chores/components/FilterSection.jsx | 55 +- src/views/Chores/hooks/useChoreFilters.js | 4 +- src/views/Chores/hooks/useCustomFilters.js | 17 +- src/views/Filters/FilterView.jsx | 823 ++++++++++++++++++ .../Modals/Inputs/AdvancedFilterBuilder.jsx | 116 ++- src/views/Modals/Inputs/SaveFilterModal.jsx | 263 ------ src/views/components/NavBar.jsx | 6 + 13 files changed, 1103 insertions(+), 434 deletions(-) create mode 100644 src/views/Filters/FilterView.jsx delete mode 100644 src/views/Modals/Inputs/SaveFilterModal.jsx diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx index 31a9636..2c36cb7 100644 --- a/src/contexts/RouterContext.jsx +++ b/src/contexts/RouterContext.jsx @@ -30,6 +30,7 @@ import PaymentCancelledView from '../views/Payments/PaymentFailView' import PaymentSuccessView from '../views/Payments/PaymentSuccessView' import PrivacyPolicyView from '../views/PrivacyPolicy/PrivacyPolicyView' import ProjectView from '../views/Projects/ProjectView' +import FilterView from '../views/Filters/FilterView' import APITokenSettings from '../views/Settings/APITokenSettings' import MFASettings from '../views/Settings/MFASettings' import NotificationSetting from '../views/Settings/NotificationSetting' @@ -233,6 +234,10 @@ const Router = createBrowserRouter([ path: 'projects/', element: , }, + { + path: 'filters/', + element: , + }, { path: '*', element: , diff --git a/src/utils/Colors.jsx b/src/utils/Colors.jsx index 87780ff..89768f0 100644 --- a/src/utils/Colors.jsx +++ b/src/utils/Colors.jsx @@ -26,6 +26,10 @@ const LABEL_COLORS = [ { name: 'Sand', value: '#d7ccc8' }, ] +export const FILTER_COLORS = [ + ...LABEL_COLORS.filter(color => color.name !== 'Default'), +] + export const COLORS = { salmon: '#ff7961', teal: '#26a69a', diff --git a/src/utils/CustomFilterStorage.js b/src/utils/CustomFilterStorage.js index a2a6695..640f687 100644 --- a/src/utils/CustomFilterStorage.js +++ b/src/utils/CustomFilterStorage.js @@ -52,6 +52,8 @@ export const saveFilter = (filter) => { 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', diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index 67eb512..2a69675 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -34,7 +34,6 @@ 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 { TASK_COLOR } from '../../utils/Colors' import Priorities from '../../utils/Priorities' import LoadingComponent from '../components/Loading' import { useLabels } from '../Labels/LabelQueries' @@ -60,7 +59,6 @@ import CalendarDual from '../components/CalendarDual' import CalendarMonthly from '../components/CalendarMonthly.jsx' import ProjectSelector from '../components/ProjectSelector' import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder' -import SaveFilterModal from '../Modals/Inputs/SaveFilterModal' import { useProjects } from '../Projects/ProjectQueries.js' import ChoreModals from './components/ChoreModals' import FilterSection from './components/FilterSection' @@ -179,7 +177,6 @@ const MyChores = () => { projectsWithDefault, ) - const [showSaveFilterModal, setShowSaveFilterModal] = useState(false) const [showAdvancedFilterBuilder, setShowAdvancedFilterBuilder] = useState(false) const [editingFilter, setEditingFilter] = useState(null) @@ -736,6 +733,10 @@ const MyChores = () => { setFilteredChores(chores) setSearchFilter('All') }} + onCreateNewFilter={() => { + setShowAdvancedFilterBuilder(true) + setEditingFilter(null) + }} mouseClickHandler={handleMenuOutsideClick} /> @@ -1100,7 +1101,7 @@ const MyChores = () => { {viewMode === 'calendar' && ( <> {/* Summary Chips when no date selected */} - { Pending Approval )} - + */} {/* Calendar Monthly View */} {isLargeScreen ? ( @@ -1467,38 +1468,6 @@ const MyChores = () => { onClose={closeModal} /> - {/* Save Filter Modal */} - setShowSaveFilterModal(false)} - onSave={filter => { - saveFilter(filter) - showSuccess({ - title: 'Filter Saved', - message: `"${filter.name}" has been saved successfully`, - }) - }} - filterData={createFilterFromCurrentState({ - selectedProject, - selectedChoreFilter, - searchFilter, - })} - previewChores={ - activeFilterId ? customFilteredChores : searchFilteredChores - } - previewCount={ - activeFilterId - ? customFilteredChores.length - : searchFilteredChores.length - } - previewOverdueCount={ - (activeFilterId ? customFilteredChores : searchFilteredChores).filter( - chore => - chore.nextDueDate && new Date(chore.nextDueDate) < new Date(), - ).length - } - /> - {/* Advanced Filter Builder */} { // Update existing filter updateFilter(filter.id, { name: filter.name, + description: filter.description, + color: filter.color, conditions: filter.conditions, operator: filter.operator, }) diff --git a/src/views/Chores/SortAndGrouping.jsx b/src/views/Chores/SortAndGrouping.jsx index e5d0ded..413b2ca 100644 --- a/src/views/Chores/SortAndGrouping.jsx +++ b/src/views/Chores/SortAndGrouping.jsx @@ -23,6 +23,7 @@ const SortAndGrouping = ({ isActive, useChips, title, + onCreateNewFilter, }) => { const [anchorEl, setAnchorEl] = useState(null) const menuRef = useRef(null) @@ -201,6 +202,22 @@ const SortAndGrouping = ({ /> Assigned to others + + + + { + onCreateNewFilter() + handleMenuClose() + // TODO: Open advanced filter builder + }} + > + + Create Custom Filter... + + + {/* // i need this but i think it have a bad UX and confusing so commenting it for now { + const navigate = useNavigate() const [menuAnchor, setMenuAnchor] = useState(null) const [selectedFilter, setSelectedFilter] = useState(null) @@ -73,6 +83,10 @@ const CustomFilterChips = ({ {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 ( 0 ? ` (${filter.overdueCount} overdue)` : ''}` + : `${filter.description ? filter.description + ' - ' : ''}${filter.count} tasks${filter.overdueCount > 0 ? ` (${filter.overdueCount} overdue)` : ''}` } placement='bottom' > - !hasWarning && onFilterClick(filter.id)} - onContextMenu={e => handleContextMenu(e, filter)} - sx={{ - cursor: hasWarning ? 'not-allowed' : 'pointer', - transition: 'all 0.2s ease', - px: 1.5, - py: 0.5, - opacity: hasWarning ? 0.7 : 1, - }} - startDecorator={ - - {filter.isPinned && ( - - )} - - {filter.count} - - - } - endDecorator={ - - {hasWarning && } - {!hasWarning && filter.overdueCount > 0 && ( - - {filter.overdueCount} - - )} - - } - > - !hasWarning && onFilterClick(filter.id)}> + handleContextMenu(e, filter)} sx={{ - whiteSpace: 'nowrap', - maxWidth: 200, - overflow: 'hidden', - textOverflow: 'ellipsis', + cursor: hasWarning ? 'not-allowed' : 'pointer', + transition: 'all 0.2s ease', + px: 1.5, + py: 0.5, + opacity: hasWarning ? 0.7 : isActive ? 1 : 0.85, + ...(hasCustomColor && { + backgroundColor: `${filter.color} !important`, + color: `${textColor} !important`, + '&:hover': { + backgroundColor: `${filter.color} !important`, + filter: 'brightness(0.95)', + opacity: 1, + }, + }), }} + startDecorator={ + + {filter.isPinned && ( + + )} + + {filter.count} + + + } + endDecorator={ + + {hasWarning && } + {!hasWarning && filter.overdueCount > 0 && ( + + {filter.overdueCount} + + )} + + } > - {filter.name} - - + + {filter.name} + + + ) })} + } + sx={{ cursor: 'pointer' }} + onClick={e => { + e.preventDefault() + e.stopPropagation() + navigate('/filters') + }} + > + Manage Filters + + { return ( <> @@ -31,53 +26,7 @@ const FilterSection = ({ )} - {/* Create Advanced Filter Button */} - {!activeFilterId && ( - - - - )} - {/* Active Custom Filter Display */} - {activeFilter && ( - - - - - } - > - Filter: {activeFilter.name} ({activeFilter.count} tasks - {activeFilter.overdueCount > 0 && - `, ${activeFilter.overdueCount} overdue`} - ) - {hasProjectConditions && ( - - Cross-Project - - )} - - - )} ) } diff --git a/src/views/Chores/hooks/useChoreFilters.js b/src/views/Chores/hooks/useChoreFilters.js index fccb5e5..8153f1d 100644 --- a/src/views/Chores/hooks/useChoreFilters.js +++ b/src/views/Chores/hooks/useChoreFilters.js @@ -1,6 +1,6 @@ -import { useState, useMemo, useCallback } from 'react' import Fuse from 'fuse.js' -import { filterByProject, ChoreFilters } from '../../../utils/Chores' +import { useCallback, useMemo, useState } from 'react' +import { ChoreFilters, filterByProject } from '../../../utils/Chores' export const useChoreFilters = ({ chores, diff --git a/src/views/Chores/hooks/useCustomFilters.js b/src/views/Chores/hooks/useCustomFilters.js index 1f3449b..2937013 100644 --- a/src/views/Chores/hooks/useCustomFilters.js +++ b/src/views/Chores/hooks/useCustomFilters.js @@ -1,20 +1,19 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useUserProfile } from '../../../queries/UserQueries' -import { - applyFilter, - getFilterCount, - getFilterOverdueCount, - validateFilter, -} from '../../../utils/FilterEngine' import { deleteFilter as deleteFilterStorage, - getFilterById, getSavedFilters, saveFilter as saveFilterStorage, toggleFilterPin, trackFilterUsage, updateFilter as updateFilterStorage, } from '../../../utils/CustomFilterStorage' +import { + applyFilter, + getFilterCount, + getFilterOverdueCount, + validateFilter, +} from '../../../utils/FilterEngine' export const useCustomFilters = (chores, membersData, labels, projects) => { const { data: userProfile } = useUserProfile() @@ -52,13 +51,15 @@ export const useCustomFilters = (chores, membersData, labels, projects) => { ? getFilterOverdueCount(chores, filter, context) : 0 - return { + const result = { ...filter, count, overdueCount, isValid: validation.isValid, validationIssues: validation.issues, } + + return result }) }, [savedFilters, chores, context]) diff --git a/src/views/Filters/FilterView.jsx b/src/views/Filters/FilterView.jsx new file mode 100644 index 0000000..3af21b2 --- /dev/null +++ b/src/views/Filters/FilterView.jsx @@ -0,0 +1,823 @@ +import DeleteIcon from '@mui/icons-material/Delete' +import EditIcon from '@mui/icons-material/Edit' +import { + Avatar, + Box, + Chip, + CircularProgress, + Container, + IconButton, + Stack, + Typography, +} from '@mui/joy' +import { useEffect, useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' + +import { Add, FilterAlt, Star, StarBorder, Task } 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' + +import { useLabels } from '../Labels/LabelQueries' +import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder' +import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' +import { useProjects } from '../Projects/ProjectQueries' + +const FilterCard = ({ + filter, + onEditClick, + onDeleteClick, + onPinClick, + taskCount = 0, + overdueCount = 0, +}) => { + const navigate = useNavigate() + + // Swipe functionality state + const [swipeTranslateX, setSwipeTranslateX] = useState(0) + const [isDragging, setIsDragging] = useState(false) + const [isSwipeRevealed, setIsSwipeRevealed] = useState(false) + const [hoverTimer, setHoverTimer] = useState(null) + const swipeThreshold = 80 + const maxSwipeDistance = 200 // Increased to fit pin + edit + delete + const dragStartX = useRef(0) + const cardRef = useRef(null) + + // Swipe gesture handlers + const handleTouchStart = e => { + dragStartX.current = e.touches[0].clientX + setIsDragging(true) + } + + const handleTouchMove = e => { + if (!isDragging) return + + const currentX = e.touches[0].clientX + const deltaX = currentX - dragStartX.current + + if (isSwipeRevealed) { + if (deltaX > 0) { + const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0) + setSwipeTranslateX(clampedDelta) + } + } else { + if (deltaX < 0) { + const clampedDelta = Math.max(deltaX, -maxSwipeDistance) + setSwipeTranslateX(clampedDelta) + } + } + } + + const handleTouchEnd = () => { + if (!isDragging) return + setIsDragging(false) + + if (isSwipeRevealed) { + if (swipeTranslateX > -swipeThreshold) { + setSwipeTranslateX(0) + setIsSwipeRevealed(false) + } else { + setSwipeTranslateX(-maxSwipeDistance) + } + } else { + if (Math.abs(swipeTranslateX) > swipeThreshold) { + setSwipeTranslateX(-maxSwipeDistance) + setIsSwipeRevealed(true) + } else { + setSwipeTranslateX(0) + setIsSwipeRevealed(false) + } + } + } + + const handleMouseDown = e => { + dragStartX.current = e.clientX + setIsDragging(true) + } + + const handleMouseMove = e => { + if (!isDragging) return + + const currentX = e.clientX + const deltaX = currentX - dragStartX.current + + if (isSwipeRevealed) { + if (deltaX > 0) { + const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0) + setSwipeTranslateX(clampedDelta) + } + } else { + if (deltaX < 0) { + const clampedDelta = Math.max(deltaX, -maxSwipeDistance) + setSwipeTranslateX(clampedDelta) + } + } + } + + const handleMouseUp = () => { + if (!isDragging) return + setIsDragging(false) + + if (isSwipeRevealed) { + if (swipeTranslateX > -swipeThreshold) { + setSwipeTranslateX(0) + setIsSwipeRevealed(false) + } else { + setSwipeTranslateX(-maxSwipeDistance) + } + } else { + if (Math.abs(swipeTranslateX) > swipeThreshold) { + setSwipeTranslateX(-maxSwipeDistance) + setIsSwipeRevealed(true) + } else { + setSwipeTranslateX(0) + setIsSwipeRevealed(false) + } + } + } + + const resetSwipe = () => { + setSwipeTranslateX(0) + setIsSwipeRevealed(false) + } + + // Hover functionality for desktop + const handleMouseEnter = () => { + if (isSwipeRevealed) return + const timer = setTimeout(() => { + setSwipeTranslateX(-maxSwipeDistance) + setIsSwipeRevealed(true) + setHoverTimer(null) + }, 800) + setHoverTimer(timer) + } + + const handleMouseLeave = () => { + if (hoverTimer) { + clearTimeout(hoverTimer) + setHoverTimer(null) + } + if (!isSwipeRevealed) { + const hideTimer = setTimeout(() => { + resetSwipe() + }, 300) + setHoverTimer(hideTimer) + } + } + + const handleActionAreaMouseEnter = () => { + if (hoverTimer) { + clearTimeout(hoverTimer) + setHoverTimer(null) + } + } + + const handleActionAreaMouseLeave = () => { + if (isSwipeRevealed) { + resetSwipe() + } + } + + // Clean up timer on unmount + useEffect(() => { + return () => { + if (hoverTimer) { + clearTimeout(hoverTimer) + } + } + }, [hoverTimer]) + + // Get condition labels for display + const getConditionSummary = () => { + if (!filter.conditions || filter.conditions.length === 0) { + return 'No conditions' + } + if (filter.conditions.length === 1) { + return '1 condition' + } + return `${filter.conditions.length} conditions` + } + + return ( + + { + if (hoverTimer) { + clearTimeout(hoverTimer) + setHoverTimer(null) + } + }} + > + {/* Action buttons underneath (revealed on swipe) */} + + { + e.stopPropagation() + resetSwipe() + onPinClick(filter.id) + }} + sx={{ + width: 40, + height: 40, + mx: 0.5, + }} + > + {filter.isPinned ? ( + + ) : ( + + )} + + + { + e.stopPropagation() + resetSwipe() + onEditClick(filter) + }} + sx={{ + width: 40, + height: 40, + mx: 0.5, + }} + > + + + + { + e.stopPropagation() + resetSwipe() + onDeleteClick(filter.id) + }} + sx={{ + width: 40, + height: 40, + mx: 0.5, + }} + > + + + + + {/* Main card content */} + { + if (isSwipeRevealed) { + resetSwipe() + return + } + // Navigate to MyChores with filter applied via URL param + navigate(`/chores?filterId=${encodeURIComponent(filter.id)}`) + }} + onTouchStart={handleTouchStart} + onTouchMove={handleTouchMove} + onTouchEnd={handleTouchEnd} + onMouseDown={handleMouseDown} + onMouseMove={handleMouseMove} + onMouseUp={handleMouseUp} + > + {/* Right drag area */} + + {/* Drag indicator dots */} + + {[...Array(3)].map((_, i) => ( + + ))} + + + + {/* Filter Icon */} + + + + + + + {/* Content - Center */} + + {/* Filter Name */} + + + {filter.name} + + {filter.isPinned && ( + + )} + + + {/* Filter Info */} + + {filter.description && ( + + {filter.description} + + )} + + } + color={overdueCount > 0 ? 'danger' : 'primary'} + sx={{ + fontSize: 10, + height: 18, + px: 0.75, + bgcolor: + overdueCount > 0 ? 'danger.softBg' : 'primary.softBg', + color: overdueCount > 0 ? 'danger.500' : 'primary.500', + }} + > + {taskCount} tasks + + + {overdueCount > 0 && ( + + {overdueCount} overdue + + )} + + } + sx={{ + fontSize: 10, + height: 18, + px: 0.75, + bgcolor: 'neutral.softBg', + color: 'neutral.600', + }} + > + {getConditionSummary()} + + + {filter.usageCount > 0 && ( + + Used {filter.usageCount}x + + )} + + + + + + ) +} + +const FilterView = () => { + const { data: userProfile } = useUserProfile() + const { data: chores = { res: [] } } = useChores(false) + const { data: labels = [] } = useLabels() + const { data: projects = [] } = useProjects() + const { data: membersData } = useCircleMembers() + + const [savedFilters, setSavedFilters] = useState([]) + 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) + } + } + + useEffect(() => { + loadFilters() + }, []) + + // Calculate task counts for each filter + useEffect(() => { + if (chores && chores.res && savedFilters.length > 0) { + const choresList = chores.res + const counts = {} + + const context = { + userId: userProfile?.id, + members: membersData?.res || [], + labels: labels || [], + projects: projects || [], + } + + savedFilters.forEach(filter => { + try { + const count = getFilterCount(choresList, filter, context) + const overdueCount = getFilterOverdueCount( + choresList, + filter, + context, + ) + counts[filter.id] = { count, overdueCount } + } catch (error) { + console.error( + `Error calculating count for filter ${filter.id}:`, + error, + ) + counts[filter.id] = { count: 0, overdueCount: 0 } + } + }) + + setFilterCounts(counts) + } + }, [ + chores, + savedFilters, + userProfile?.id, + labels, + projects, + membersData?.res, + ]) + + const handleAddFilter = () => { + setEditingFilter(null) + setShowAdvancedFilterBuilder(true) + } + + const handleEditFilter = filter => { + setEditingFilter(filter) + setShowAdvancedFilterBuilder(true) + } + + const handleDeleteClicked = id => { + const filter = savedFilters.find(f => f.id === id) + setConfirmationModel({ + isOpen: true, + title: 'Delete Filter', + message: `Are you sure you want to delete "${filter?.name}"? This cannot be undone.`, + confirmText: 'Delete', + color: 'danger', + cancelText: 'Cancel', + onClose: confirmed => { + if (confirmed === true) { + handleDeleteFilter(id) + } + setConfirmationModel({}) + }, + }) + } + + 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) + } + } + + const handlePinFilter = id => { + try { + toggleFilterPin(id) + loadFilters() + } catch (error) { + console.error('Error pinning filter:', error) + } + } + + 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 (isLoading) { + return ( + + + + ) + } + + return ( + + + + + Filters + + + Save your favorite filter combinations for quick access. Create + custom views to organize and find tasks faster. + + + + + + {savedFilters.length === 0 ? ( + + + + No saved filters yet + + + Create custom filters to quickly access your most used chore + + + ) : ( + savedFilters.map(filter => ( + + )) + )} + + + {showAdvancedFilterBuilder && ( + { + setShowAdvancedFilterBuilder(false) + setEditingFilter(null) + }} + onSave={handleSaveFilter} + members={membersData?.res || []} + labels={labels || []} + projects={projects || []} + allChores={chores?.res || []} + userProfile={userProfile} + editingFilter={editingFilter} + /> + )} + + + + + + + + + + ) +} + +export default FilterView diff --git a/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx b/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx index ae4cfc5..9be4f0a 100644 --- a/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx +++ b/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx @@ -9,10 +9,12 @@ import { ListItem, Option, Select, + Textarea, Typography, } from '@mui/joy' import { useEffect, useMemo, useState } from 'react' import { useResponsiveModal } from '../../../hooks/useResponsiveModal' +import { FILTER_COLORS } from '../../../utils/Colors' import { filterNameExists } from '../../../utils/CustomFilterStorage' import { applyFilter } from '../../../utils/FilterEngine' import Priorities from '../../../utils/Priorities' @@ -30,19 +32,36 @@ const AdvancedFilterBuilder = ({ }) => { const { ResponsiveModal } = useResponsiveModal() const [filterName, setFilterName] = useState('') + const [filterDescription, setFilterDescription] = useState('') + const [filterColor, setFilterColor] = useState(FILTER_COLORS[0].value) const [conditions, setConditions] = useState([ { type: 'assignee', operator: 'is', value: [] }, ]) const [error, setError] = useState('') + const [existedFilters] = useState(() => { + const storedFilters = localStorage.getItem('customFilters') + return storedFilters ? JSON.parse(storedFilters) : [] + }) // Initialize state when editing a filter useEffect(() => { if (editingFilter) { setFilterName(editingFilter.name) + setFilterDescription(editingFilter.description || '') + setFilterColor(editingFilter.color || FILTER_COLORS[0].value) setConditions(editingFilter.conditions || []) setError('') } else { setFilterName('') + setFilterDescription('') + // find color no filter has it : + const potentialColor = FILTER_COLORS.find( + color => !existedFilters.some(filter => filter.color === color.value), + ) + + setFilterColor( + potentialColor ? potentialColor.value : FILTER_COLORS[0].value, + ) setConditions([{ type: 'assignee', operator: 'is', value: [] }]) setError('') } @@ -127,6 +146,8 @@ const AdvancedFilterBuilder = ({ const filterData = { name: filterName.trim(), + description: filterDescription.trim(), + color: filterColor, conditions: validConditions, operator: 'AND', } @@ -375,11 +396,27 @@ const AdvancedFilterBuilder = ({ } return ( - - - {editingFilter ? 'Edit Filter' : 'Create Advanced Filter'} - - + + + + + } + > + + + Description (Optional) + +