feat: enhance filter functionality and UI improvements
- Added FilterView component for managing custom filters. - Implemented temporary filter application in MyChores and custom filter hooks. - Improved sorting of archived chores by updated date. - Enhanced SortAndGrouping component with keyboard navigation support. - Updated CustomFilterChips to visually indicate active filters. - Refactored AdvancedFilterBuilder to improve usability and added listbox positioning. - Integrated project selection caching in ProjectView for better navigation. - Cleaned up unused imports and optimized existing code for better performance.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { useLayoutEffect, useRef, useState } from 'react'
|
||||
import { flushSync } from 'react-dom'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import './PageTransition.css'
|
||||
|
||||
@@ -23,6 +23,7 @@ import ArchivedTasks from '../views/Chores/ArchivedTasks'
|
||||
import MyChores from '../views/Chores/MyChores'
|
||||
import JoinCircleView from '../views/Circles/JoinCircle'
|
||||
import NotFound from '../views/components/NotFound'
|
||||
import FilterView from '../views/Filters/FilterView'
|
||||
import ChoreHistory from '../views/History/ChoreHistory'
|
||||
import LabelView from '../views/Labels/LabelView'
|
||||
import Landing from '../views/Landing/Landing'
|
||||
@@ -30,7 +31,6 @@ 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'
|
||||
|
||||
@@ -8,17 +8,10 @@
|
||||
const STORAGE_KEY = 'customFilters'
|
||||
const MAX_FILTERS = 20 // Limit to prevent localStorage overflow
|
||||
|
||||
/**
|
||||
* Generate a unique filter ID
|
||||
*/
|
||||
const generateFilterId = () => {
|
||||
return `filter_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all saved filters
|
||||
* @returns {Array} - Array of filter objects
|
||||
*/
|
||||
export const getSavedFilters = () => {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
@@ -26,7 +19,6 @@ export const getSavedFilters = () => {
|
||||
|
||||
const filters = JSON.parse(stored)
|
||||
|
||||
// Ensure filters have required fields
|
||||
return filters.filter(f => f.id && f.name && f.conditions)
|
||||
} catch (error) {
|
||||
console.error('Error loading saved filters:', error)
|
||||
@@ -34,18 +26,15 @@ export const getSavedFilters = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new filter
|
||||
* @param {Object} filter - The filter to save
|
||||
* @returns {Object} - The saved filter with generated ID and metadata
|
||||
*/
|
||||
export const saveFilter = (filter) => {
|
||||
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.`)
|
||||
throw new Error(
|
||||
`Maximum of ${MAX_FILTERS} filters allowed. Please delete some filters first.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Create new filter with metadata
|
||||
@@ -61,7 +50,7 @@ export const saveFilter = (filter) => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
isPinned: filter.isPinned || false,
|
||||
usageCount: 0,
|
||||
lastUsedAt: null
|
||||
lastUsedAt: null,
|
||||
}
|
||||
|
||||
// Add to filters array
|
||||
@@ -77,12 +66,7 @@ export const saveFilter = (filter) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing filter
|
||||
* @param {string} filterId - The ID of the filter to update
|
||||
* @param {Object} updates - The fields to update
|
||||
* @returns {Object} - The updated filter
|
||||
*/
|
||||
|
||||
export const updateFilter = (filterId, updates) => {
|
||||
try {
|
||||
const filters = getSavedFilters()
|
||||
@@ -96,7 +80,7 @@ export const updateFilter = (filterId, updates) => {
|
||||
const updatedFilter = {
|
||||
...filters[filterIndex],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString()
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
filters[filterIndex] = updatedFilter
|
||||
@@ -111,12 +95,7 @@ export const updateFilter = (filterId, updates) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a filter
|
||||
* @param {string} filterId - The ID of the filter to delete
|
||||
* @returns {boolean} - Success status
|
||||
*/
|
||||
export const deleteFilter = (filterId) => {
|
||||
export const deleteFilter = filterId => {
|
||||
try {
|
||||
const filters = getSavedFilters()
|
||||
const updatedFilters = filters.filter(f => f.id !== filterId)
|
||||
@@ -130,27 +109,20 @@ export const deleteFilter = (filterId) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single filter by ID
|
||||
* @param {string} filterId - The ID of the filter
|
||||
* @returns {Object|null} - The filter object or null if not found
|
||||
*/
|
||||
export const getFilterById = (filterId) => {
|
||||
|
||||
export const getFilterById = filterId => {
|
||||
const filters = getSavedFilters()
|
||||
return filters.find(f => f.id === filterId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment usage count for a filter
|
||||
* @param {string} filterId - The ID of the filter
|
||||
*/
|
||||
export const trackFilterUsage = (filterId) => {
|
||||
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].usageCount =
|
||||
(filters[filterIndex].usageCount || 0) + 1
|
||||
filters[filterIndex].lastUsedAt = new Date().toISOString()
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(filters))
|
||||
@@ -160,12 +132,8 @@ export const trackFilterUsage = (filterId) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle pin status of a filter
|
||||
* @param {string} filterId - The ID of the filter
|
||||
* @returns {boolean} - New pin status
|
||||
*/
|
||||
export const toggleFilterPin = (filterId) => {
|
||||
|
||||
export const toggleFilterPin = filterId => {
|
||||
try {
|
||||
const filters = getSavedFilters()
|
||||
const filterIndex = filters.findIndex(f => f.id === filterId)
|
||||
@@ -186,53 +154,31 @@ export const toggleFilterPin = (filterId) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filters sorted by usage (most used first)
|
||||
* @returns {Array} - Sorted filters
|
||||
*/
|
||||
|
||||
export const getFiltersByUsage = () => {
|
||||
const filters = getSavedFilters()
|
||||
return filters.sort((a, b) => (b.usageCount || 0) - (a.usageCount || 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pinned filters
|
||||
* @returns {Array} - Pinned filters
|
||||
*/
|
||||
|
||||
export const getPinnedFilters = () => {
|
||||
const filters = getSavedFilters()
|
||||
return filters.filter(f => f.isPinned)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if filter name already exists
|
||||
* @param {string} name - The filter name to check
|
||||
* @param {string} excludeId - Optional ID to exclude from check (for updates)
|
||||
* @returns {boolean} - Whether the name exists
|
||||
*/
|
||||
export const filterNameExists = (name, excludeId = null) => {
|
||||
const filters = getSavedFilters()
|
||||
return filters.some(f =>
|
||||
f.name.toLowerCase() === name.toLowerCase() &&
|
||||
f.id !== excludeId
|
||||
return filters.some(
|
||||
f => f.name.toLowerCase() === name.toLowerCase() && f.id !== excludeId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export filters as JSON (for backup/sharing)
|
||||
* @returns {string} - JSON string of all filters
|
||||
*/
|
||||
export const exportFilters = () => {
|
||||
const filters = getSavedFilters()
|
||||
return JSON.stringify(filters, null, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Import filters from JSON
|
||||
* @param {string} jsonString - JSON string of filters
|
||||
* @returns {number} - Number of filters imported
|
||||
*/
|
||||
export const importFilters = (jsonString) => {
|
||||
export const importFilters = jsonString => {
|
||||
try {
|
||||
const importedFilters = JSON.parse(jsonString)
|
||||
|
||||
@@ -249,7 +195,7 @@ export const importFilters = (jsonString) => {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
usageCount: 0,
|
||||
lastUsedAt: null
|
||||
lastUsedAt: null,,
|
||||
}))
|
||||
|
||||
const allFilters = [...existingFilters, ...newFilters]
|
||||
@@ -268,10 +214,7 @@ export const importFilters = (jsonString) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters (use with caution!)
|
||||
* @returns {boolean} - Success status
|
||||
*/
|
||||
|
||||
export const clearAllFilters = () => {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useUnArchiveChore } from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { ChoreSorter } from '../../utils/Chores'
|
||||
import { DeleteChore, GetArchivedChores } from '../../utils/Fetcher'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
@@ -68,7 +67,12 @@ const ArchivedTasks = () => {
|
||||
try {
|
||||
const response = await GetArchivedChores()
|
||||
const data = await response.json()
|
||||
const sortedChores = data.res.sort(ChoreSorter)
|
||||
// Sort by updatedAt (most recent first)
|
||||
const sortedChores = data.res.sort((a, b) => {
|
||||
const dateA = new Date(a.updatedAt || 0)
|
||||
const dateB = new Date(b.updatedAt || 0)
|
||||
return dateB - dateA
|
||||
})
|
||||
setArchivedChores(sortedChores)
|
||||
setFilteredChores(sortedChores)
|
||||
} catch (error) {
|
||||
|
||||
@@ -161,15 +161,19 @@ const MyChores = () => {
|
||||
savedFilters,
|
||||
activeFilter,
|
||||
activeFilterId,
|
||||
tempFilter,
|
||||
filteredChores: customFilteredChores,
|
||||
applyCustomFilter,
|
||||
clearActiveFilter,
|
||||
applyTempFilter,
|
||||
clearTempFilter,
|
||||
saveFilter,
|
||||
updateFilter,
|
||||
deleteFilter,
|
||||
pinFilter,
|
||||
createFilterFromCurrentState,
|
||||
hasProjectConditions,
|
||||
hasFilterApplied,
|
||||
} = useCustomFilters(
|
||||
nonProjectFilteredChores,
|
||||
membersData?.res,
|
||||
@@ -205,9 +209,12 @@ const MyChores = () => {
|
||||
return []
|
||||
}
|
||||
|
||||
// Use project-filtered chores for section grouping
|
||||
// If a custom filter (temp or saved) is active, use customFilteredChores
|
||||
let choresToGroup = chores
|
||||
if (selectedProject) {
|
||||
if (tempFilter || activeFilterId) {
|
||||
choresToGroup = customFilteredChores
|
||||
} else if (selectedProject) {
|
||||
// Otherwise, use project-filtered chores for section grouping
|
||||
if (selectedProject.id === 'default') {
|
||||
// Default project: only show tasks without a projectId
|
||||
choresToGroup = chores.filter(chore => !chore.projectId)
|
||||
@@ -228,6 +235,9 @@ const MyChores = () => {
|
||||
return sections
|
||||
}, [
|
||||
chores,
|
||||
customFilteredChores,
|
||||
tempFilter,
|
||||
activeFilterId,
|
||||
selectedChoreSection,
|
||||
selectedChoreFilter,
|
||||
selectedProject,
|
||||
@@ -325,6 +335,32 @@ const MyChores = () => {
|
||||
}
|
||||
}, [searchInputFocus])
|
||||
|
||||
// Read and apply project from URL parameters
|
||||
useEffect(() => {
|
||||
if (!projects.length) return
|
||||
|
||||
const projectIdFromUrl = searchParams.get('project')
|
||||
|
||||
if (projectIdFromUrl && projectIdFromUrl !== selectedProject?.id) {
|
||||
const project = projectsWithDefault.find(p => p.id === projectIdFromUrl)
|
||||
if (project) {
|
||||
setSelectedProjectWithCache(project)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
searchParams,
|
||||
projects,
|
||||
projectsWithDefault,
|
||||
selectedProject,
|
||||
setSelectedProjectWithCache,
|
||||
|
||||
searchParams,
|
||||
projects,
|
||||
projectsWithDefault,
|
||||
selectedProject,
|
||||
setSelectedProjectWithCache,
|
||||
])
|
||||
|
||||
// Read and apply filters from URL parameters
|
||||
useEffect(() => {
|
||||
if (!chores.length || !savedFilters.length) return
|
||||
@@ -332,6 +368,7 @@ const MyChores = () => {
|
||||
// Check for filterId (camelCase) or filter_id (snake_case) for advanced filters
|
||||
const filterId =
|
||||
searchParams.get('filterId') || searchParams.get('filter_id')
|
||||
|
||||
const oldFilter = searchParams.get('filter')
|
||||
|
||||
// Handle advanced filter parameter
|
||||
@@ -505,10 +542,7 @@ const MyChores = () => {
|
||||
}
|
||||
|
||||
// Always navigate with params (preserves project param)
|
||||
const paramString = params.toString()
|
||||
Navigate(paramString ? `/chores?${paramString}` : '/chores', {
|
||||
replace: true,
|
||||
})
|
||||
Navigate({ pathname: '/chores', search: params.toString() })
|
||||
}
|
||||
|
||||
const searchOptions = useMemo(
|
||||
@@ -617,7 +651,7 @@ const MyChores = () => {
|
||||
}
|
||||
|
||||
const getFilteredChores = useMemo(() => {
|
||||
if (activeFilterId) {
|
||||
if (activeFilterId || tempFilter) {
|
||||
return customFilteredChores
|
||||
}
|
||||
|
||||
@@ -644,6 +678,7 @@ const MyChores = () => {
|
||||
return baseChores
|
||||
}, [
|
||||
activeFilterId,
|
||||
tempFilter,
|
||||
customFilteredChores,
|
||||
projectFilteredChores,
|
||||
searchTerm,
|
||||
@@ -741,16 +776,18 @@ const MyChores = () => {
|
||||
/>
|
||||
|
||||
{/* Project Selector - Hidden when active filter has project conditions */}
|
||||
{projectsWithDefault.length > 1 && !hasProjectConditions && (
|
||||
<ProjectSelector
|
||||
selectedProject={selectedProject?.name || 'Default Project'}
|
||||
onProjectSelect={project => {
|
||||
setSelectedProjectWithCache(project)
|
||||
clearActiveFilter()
|
||||
}}
|
||||
showKeyboardShortcuts={showKeyboardShortcuts}
|
||||
/>
|
||||
)}
|
||||
{projectsWithDefault.length > 1 &&
|
||||
!hasProjectConditions &&
|
||||
!hasFilterApplied && (
|
||||
<ProjectSelector
|
||||
selectedProject={selectedProject?.name || 'Default Project'}
|
||||
onProjectSelect={project => {
|
||||
setSelectedProjectWithCache(project)
|
||||
clearActiveFilter()
|
||||
}}
|
||||
showKeyboardShortcuts={showKeyboardShortcuts}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* View Mode Toggle Button */}
|
||||
<IconButton
|
||||
@@ -1446,7 +1483,14 @@ const MyChores = () => {
|
||||
)}
|
||||
</Container>
|
||||
|
||||
<Sidepanel chores={chores} performers={membersData?.res || []} />
|
||||
<Sidepanel
|
||||
chores={customFilteredChores}
|
||||
allChores={chores}
|
||||
performers={membersData?.res || []}
|
||||
applyTempFilter={applyTempFilter}
|
||||
clearTempFilter={clearTempFilter}
|
||||
tempFilter={tempFilter}
|
||||
/>
|
||||
|
||||
{/* Multi-select Help - only show when in multi-select mode */}
|
||||
{/* <MultiSelectHelp isVisible={isMultiSelectMode} /> */}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Add, Check } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Divider,
|
||||
ListItemContent,
|
||||
ListItemDecorator,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Radio,
|
||||
@@ -9,7 +12,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import IconButton from '@mui/joy/IconButton'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||
|
||||
const SortAndGrouping = ({
|
||||
label,
|
||||
@@ -26,10 +29,15 @@ const SortAndGrouping = ({
|
||||
onCreateNewFilter,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [isKeyboardNavigating, setIsKeyboardNavigating] = useState(false)
|
||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||
const menuRef = useRef(null)
|
||||
const buttonRef = useRef(null)
|
||||
|
||||
const handleMenuOpen = event => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
setIsKeyboardNavigating(false)
|
||||
}
|
||||
|
||||
const handleMenuClose = () => {
|
||||
@@ -49,37 +57,177 @@ const SortAndGrouping = ({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Keyboard shortcut handler
|
||||
useEffect(() => {
|
||||
const handleKeyDown = event => {
|
||||
const isHoldingCmdOrCtrl = event.ctrlKey || event.metaKey
|
||||
|
||||
// Cmd/Ctrl + G to open sort menu
|
||||
if (isHoldingCmdOrCtrl && event.key === 'g') {
|
||||
event.preventDefault()
|
||||
if (!anchorEl) {
|
||||
setAnchorEl(buttonRef.current)
|
||||
setSelectedIndex(0)
|
||||
setIsKeyboardNavigating(true)
|
||||
} else {
|
||||
handleMenuClose()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Only handle navigation keys when menu is open
|
||||
if (!anchorEl) return
|
||||
|
||||
const groupByItems = [
|
||||
{ name: 'Smart', value: 'default' },
|
||||
{ name: 'Due Date', value: 'due_date' },
|
||||
{ name: 'Priority', value: 'priority' },
|
||||
{ name: 'Labels', value: 'labels' },
|
||||
]
|
||||
|
||||
const filterItems = ['anyone', 'assigned_to_me', 'assigned_to_others']
|
||||
|
||||
// Total selectable items: 4 (group by) + 3 (filters) + 1 (create custom filter) = 8
|
||||
const totalItems = groupByItems.length + filterItems.length + 1
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
setIsKeyboardNavigating(true)
|
||||
setSelectedIndex(prev => (prev < totalItems - 1 ? prev + 1 : prev))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
setIsKeyboardNavigating(true)
|
||||
setSelectedIndex(prev => (prev > 0 ? prev - 1 : prev))
|
||||
break
|
||||
case 'Enter':
|
||||
event.preventDefault()
|
||||
if (selectedIndex < groupByItems.length) {
|
||||
// Group by items (0-3)
|
||||
const item = groupByItems[selectedIndex]
|
||||
onItemSelect(item)
|
||||
setSelectedItem?.(item.name)
|
||||
handleMenuClose()
|
||||
} else if (selectedIndex < groupByItems.length + filterItems.length) {
|
||||
// Filter items (4-6)
|
||||
const filterIndex = selectedIndex - groupByItems.length
|
||||
setFilter(filterItems[filterIndex])
|
||||
handleMenuClose()
|
||||
} else {
|
||||
// Create custom filter (7)
|
||||
onCreateNewFilter()
|
||||
handleMenuClose()
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
event.preventDefault()
|
||||
handleMenuClose()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [
|
||||
anchorEl,
|
||||
selectedIndex,
|
||||
onItemSelect,
|
||||
setSelectedItem,
|
||||
setFilter,
|
||||
onCreateNewFilter,
|
||||
])
|
||||
|
||||
// Reset selected index when menu opens
|
||||
useEffect(() => {
|
||||
if (anchorEl) {
|
||||
setSelectedIndex(0)
|
||||
}
|
||||
}, [anchorEl])
|
||||
|
||||
// Keyboard shortcut hint handler
|
||||
useEffect(() => {
|
||||
const handleKeyDown = event => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setShowKeyboardShortcuts(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = event => {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
setShowKeyboardShortcuts(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.addEventListener('keyup', handleKeyUp)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('keyup', handleKeyUp)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{!label && (
|
||||
<IconButton
|
||||
onClick={handleMenuOpen}
|
||||
variant='outlined'
|
||||
color={isActive ? 'primary' : 'neutral'}
|
||||
size='sm'
|
||||
sx={{
|
||||
height: 24,
|
||||
borderRadius: 24,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{label ? label : null}
|
||||
</IconButton>
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<IconButton
|
||||
ref={buttonRef}
|
||||
onClick={handleMenuOpen}
|
||||
variant='outlined'
|
||||
color={isActive ? 'primary' : 'neutral'}
|
||||
size='sm'
|
||||
sx={{
|
||||
height: 24,
|
||||
borderRadius: 24,
|
||||
}}
|
||||
title='Sort and Group (Ctrl+G)'
|
||||
>
|
||||
{icon}
|
||||
{label ? label : null}
|
||||
</IconButton>
|
||||
<KeyboardShortcutHint
|
||||
shortcut='G'
|
||||
show={showKeyboardShortcuts}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: -8,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{label && (
|
||||
<Button
|
||||
onClick={handleMenuOpen}
|
||||
variant='outlined'
|
||||
color={isActive ? 'primary' : 'neutral'}
|
||||
size='sm'
|
||||
startDecorator={icon}
|
||||
sx={{
|
||||
height: 24,
|
||||
borderRadius: 24,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
onClick={handleMenuOpen}
|
||||
variant='outlined'
|
||||
color={isActive ? 'primary' : 'neutral'}
|
||||
size='sm'
|
||||
startDecorator={icon}
|
||||
sx={{
|
||||
height: 24,
|
||||
borderRadius: 24,
|
||||
}}
|
||||
title='Sort and Group (Ctrl+G)'
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
<KeyboardShortcutHint
|
||||
shortcut='G'
|
||||
show={showKeyboardShortcuts}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: -8,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Menu
|
||||
@@ -88,24 +236,39 @@ const SortAndGrouping = ({
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
placement='bottom-start'
|
||||
sx={{
|
||||
'& .MuiMenuItem-root': {
|
||||
padding: '8px 16px', // Consistent padding for menu items
|
||||
},
|
||||
minWidth: 280,
|
||||
p: 1,
|
||||
'--List-gap': '4px',
|
||||
boxShadow: 'var(--joy-shadow-lg)',
|
||||
border: '1px solid var(--joy-palette-divider)',
|
||||
borderRadius: 'var(--joy-radius-md)',
|
||||
}}
|
||||
>
|
||||
<MenuItem key={`${k}-title`} disabled>
|
||||
<Typography level='body-sm' fontWeight='lg'>
|
||||
Group By
|
||||
</Typography>
|
||||
<MenuItem
|
||||
disabled
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
cursor: 'default',
|
||||
opacity: 1,
|
||||
}}
|
||||
>
|
||||
<ListItemContent>
|
||||
<Typography level='title-sm' sx={{ fontWeight: 600 }}>
|
||||
{title || 'Group By'}
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
|
||||
{[
|
||||
{ name: 'Smart', value: 'default' },
|
||||
{ name: 'Due Date', value: 'due_date' },
|
||||
{ name: 'Priority', value: 'priority' },
|
||||
{ name: 'Labels', value: 'labels' },
|
||||
].map(item => (
|
||||
].map((item, index) => (
|
||||
<MenuItem
|
||||
key={`${k}-${item?.value}`}
|
||||
onClick={() => {
|
||||
@@ -113,41 +276,87 @@ const SortAndGrouping = ({
|
||||
setSelectedItem?.(item.name)
|
||||
handleMenuClose()
|
||||
}}
|
||||
onMouseEnter={() => setIsKeyboardNavigating(false)}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
backgroundColor:
|
||||
selectedItem === item.name
|
||||
? 'var(--joy-palette-primary-softBg)'
|
||||
: selectedIndex === index && anchorEl && isKeyboardNavigating
|
||||
? 'var(--joy-palette-neutral-softHoverBg)'
|
||||
: 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
selectedItem === item.name
|
||||
? 'var(--joy-palette-primary-softBg)'
|
||||
: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{useChips ? (
|
||||
<Chip
|
||||
size='sm'
|
||||
<ListItemContent>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: item.color ? item.color : null,
|
||||
color: getTextColorFromBackgroundColor(item.color),
|
||||
fontWeight: 'md',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Chip>
|
||||
) : (
|
||||
<>
|
||||
{item?.icon}
|
||||
<Typography level='body-sm' sx={{ ml: 1 }}>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
fontWeight: selectedItem === item.name ? 600 : 400,
|
||||
color:
|
||||
selectedItem === item.name
|
||||
? 'var(--joy-palette-primary-600)'
|
||||
: 'var(--joy-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{selectedItem === item.name && (
|
||||
<Check
|
||||
sx={{
|
||||
fontSize: '16px',
|
||||
color: 'var(--joy-palette-primary-500)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<Divider />
|
||||
<Divider sx={{ my: 1 }} />
|
||||
|
||||
<MenuItem key={`${k}-quick-filter`} disabled>
|
||||
<Typography level='body-sm' fontWeight='lg'>
|
||||
Quick Filters
|
||||
</Typography>
|
||||
<MenuItem
|
||||
disabled
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
cursor: 'default',
|
||||
opacity: 1,
|
||||
}}
|
||||
>
|
||||
<ListItemContent>
|
||||
<Typography level='title-sm' sx={{ fontWeight: 600 }}>
|
||||
Quick Filters
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem key={`${k}-assignee-title`} disabled>
|
||||
<Typography level='body-xs' fontWeight='md'>
|
||||
Assigned to :
|
||||
</Typography>
|
||||
<MenuItem
|
||||
disabled
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
cursor: 'default',
|
||||
opacity: 1,
|
||||
paddingY: '4px',
|
||||
}}
|
||||
>
|
||||
<ListItemContent>
|
||||
<Typography level='body-xs' sx={{ fontWeight: 600 }}>
|
||||
Assigned to:
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
@@ -156,9 +365,48 @@ const SortAndGrouping = ({
|
||||
setFilter('anyone')
|
||||
handleMenuClose()
|
||||
}}
|
||||
onMouseEnter={() => setIsKeyboardNavigating(false)}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
backgroundColor:
|
||||
selectedFilter === 'anyone'
|
||||
? 'var(--joy-palette-primary-softBg)'
|
||||
: selectedIndex === 4 && anchorEl && isKeyboardNavigating
|
||||
? 'var(--joy-palette-neutral-softHoverBg)'
|
||||
: 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
selectedFilter === 'anyone'
|
||||
? 'var(--joy-palette-primary-softBg)'
|
||||
: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Radio checked={selectedFilter === 'anyone'} variant='outlined' />
|
||||
<Typography level='body-sm'>Anyone</Typography>
|
||||
<ListItemDecorator>
|
||||
<Radio checked={selectedFilter === 'anyone'} variant='outlined' />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
fontWeight: selectedFilter === 'anyone' ? 600 : 400,
|
||||
color:
|
||||
selectedFilter === 'anyone'
|
||||
? 'var(--joy-palette-primary-600)'
|
||||
: 'var(--joy-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
Anyone
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
@@ -167,27 +415,52 @@ const SortAndGrouping = ({
|
||||
setFilter('assigned_to_me')
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
checked={selectedFilter === 'assigned_to_me'}
|
||||
variant='outlined'
|
||||
/>
|
||||
<Typography level='body-sm'>Assigned to me</Typography>
|
||||
</MenuItem>
|
||||
|
||||
{/* <MenuItem
|
||||
key={`${k}-assignee-assignable-to-me`}
|
||||
onClick={() => {
|
||||
setFilter('assignable_to_me')
|
||||
handleMenuClose()
|
||||
onMouseEnter={() => setIsKeyboardNavigating(false)}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
backgroundColor:
|
||||
selectedFilter === 'assigned_to_me'
|
||||
? 'var(--joy-palette-primary-softBg)'
|
||||
: selectedIndex === 5 && anchorEl && isKeyboardNavigating
|
||||
? 'var(--joy-palette-neutral-softHoverBg)'
|
||||
: 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
selectedFilter === 'assigned_to_me'
|
||||
? 'var(--joy-palette-primary-softBg)'
|
||||
: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
checked={selectedFilter === 'assignable_to_me'}
|
||||
variant='outlined'
|
||||
/>
|
||||
<Typography level='body-sm'>Available for me</Typography>
|
||||
</MenuItem> */}
|
||||
<ListItemDecorator>
|
||||
<Radio
|
||||
checked={selectedFilter === 'assigned_to_me'}
|
||||
variant='outlined'
|
||||
/>
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
fontWeight: selectedFilter === 'assigned_to_me' ? 600 : 400,
|
||||
color:
|
||||
selectedFilter === 'assigned_to_me'
|
||||
? 'var(--joy-palette-primary-600)'
|
||||
: 'var(--joy-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
Assigned to me
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
key={`${k}-assignee-assigned-to-others`}
|
||||
@@ -195,44 +468,94 @@ const SortAndGrouping = ({
|
||||
setFilter('assigned_to_others')
|
||||
handleMenuClose()
|
||||
}}
|
||||
onMouseEnter={() => setIsKeyboardNavigating(false)}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
backgroundColor:
|
||||
selectedFilter === 'assigned_to_others'
|
||||
? 'var(--joy-palette-primary-softBg)'
|
||||
: selectedIndex === 6 && anchorEl && isKeyboardNavigating
|
||||
? 'var(--joy-palette-neutral-softHoverBg)'
|
||||
: 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
selectedFilter === 'assigned_to_others'
|
||||
? 'var(--joy-palette-primary-softBg)'
|
||||
: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
checked={selectedFilter === 'assigned_to_others'}
|
||||
variant='outlined'
|
||||
/>
|
||||
<Typography level='body-sm'>Assigned to others</Typography>
|
||||
<ListItemDecorator>
|
||||
<Radio
|
||||
checked={selectedFilter === 'assigned_to_others'}
|
||||
variant='outlined'
|
||||
/>
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
fontWeight:
|
||||
selectedFilter === 'assigned_to_others' ? 600 : 400,
|
||||
color:
|
||||
selectedFilter === 'assigned_to_others'
|
||||
? 'var(--joy-palette-primary-600)'
|
||||
: 'var(--joy-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
Assigned to others
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
<Divider />
|
||||
<Divider sx={{ my: 1 }} />
|
||||
|
||||
<MenuItem
|
||||
key={`${k}-custom-filter`}
|
||||
onClick={() => {
|
||||
onCreateNewFilter()
|
||||
handleMenuClose()
|
||||
// TODO: Open advanced filter builder
|
||||
}}
|
||||
onMouseEnter={() => setIsKeyboardNavigating(false)}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
backgroundColor:
|
||||
selectedIndex === 7 && anchorEl && isKeyboardNavigating
|
||||
? 'var(--joy-palette-success-softHoverBg)'
|
||||
: 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-success-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm' fontWeight='md' color='primary'>
|
||||
Create Custom Filter...
|
||||
</Typography>
|
||||
<ListItemDecorator sx={{ color: 'var(--joy-palette-success-500)' }}>
|
||||
<Add />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Create Custom Filter
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||
>
|
||||
Build advanced filter rules
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
{/*
|
||||
// i need this but i think it have a bad UX and confusing so commenting it for now
|
||||
<MenuItem
|
||||
key={`${k}-assignee-created-by-me`}
|
||||
onClick={() => {
|
||||
setFilter('created_by_me')
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
checked={selectedFilter === 'created_by_me'}
|
||||
variant='outlined'
|
||||
/>
|
||||
<Typography level='body-sm'>Created by me</Typography>
|
||||
</MenuItem> */}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Check,
|
||||
Delete,
|
||||
Edit,
|
||||
Settings,
|
||||
@@ -129,33 +130,40 @@ const CustomFilterChips = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='solid'
|
||||
sx={{
|
||||
...(hasCustomColor
|
||||
? {
|
||||
bgcolor:
|
||||
textColor === '#FFFFFF'
|
||||
? '#00000040'
|
||||
: '#FFFFFF40',
|
||||
color: textColor,
|
||||
border: `1px solid ${textColor}30`,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
color={
|
||||
hasCustomColor
|
||||
? undefined
|
||||
: hasWarning
|
||||
? 'warning'
|
||||
: isActive
|
||||
? 'primary'
|
||||
{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>
|
||||
}
|
||||
>
|
||||
{filter.count}
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
endDecorator={
|
||||
@@ -206,15 +214,14 @@ const CustomFilterChips = ({
|
||||
<Chip
|
||||
variant='outlined'
|
||||
size='lg'
|
||||
startDecorator={<Settings />}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
sx={{ cursor: 'pointer', minWidth: 'auto', px: 0.8 }}
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
navigate('/filters')
|
||||
}}
|
||||
>
|
||||
Manage Filters
|
||||
<Settings />
|
||||
</Chip>
|
||||
|
||||
<Menu
|
||||
|
||||
@@ -19,6 +19,7 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const [savedFilters, setSavedFilters] = useState([])
|
||||
const [activeFilterId, setActiveFilterId] = useState(null)
|
||||
const [tempFilter, setTempFilter] = useState(null)
|
||||
|
||||
const loadFilters = useCallback(() => {
|
||||
const filters = getSavedFilters()
|
||||
@@ -74,12 +75,21 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
|
||||
return activeFilter.conditions.some(c => c.type === 'project')
|
||||
}, [activeFilter])
|
||||
|
||||
// check if has any filter applied:
|
||||
const hasFilterApplied = useMemo(() => {
|
||||
return activeFilterId !== null || tempFilter !== null
|
||||
}, [activeFilterId, tempFilter])
|
||||
|
||||
const filteredChores = useMemo(() => {
|
||||
// Temporary filter takes precedence over saved filters
|
||||
if (tempFilter) {
|
||||
return applyFilter(chores, tempFilter, context)
|
||||
}
|
||||
if (!activeFilter || !activeFilter.isValid) {
|
||||
return chores
|
||||
}
|
||||
return applyFilter(chores, activeFilter, context)
|
||||
}, [chores, activeFilter, context])
|
||||
}, [chores, activeFilter, tempFilter, context])
|
||||
|
||||
const applyCustomFilter = useCallback(
|
||||
filterId => {
|
||||
@@ -92,6 +102,16 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
|
||||
|
||||
const clearActiveFilter = useCallback(() => {
|
||||
setActiveFilterId(null)
|
||||
setTempFilter(null)
|
||||
}, [])
|
||||
|
||||
const applyTempFilter = useCallback(filter => {
|
||||
setTempFilter(filter)
|
||||
setActiveFilterId(null)
|
||||
}, [])
|
||||
|
||||
const clearTempFilter = useCallback(() => {
|
||||
setTempFilter(null)
|
||||
}, [])
|
||||
|
||||
const saveFilter = useCallback(
|
||||
@@ -228,14 +248,18 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
|
||||
savedFilters: filtersWithCounts,
|
||||
activeFilter,
|
||||
activeFilterId,
|
||||
tempFilter,
|
||||
filteredChores,
|
||||
applyCustomFilter,
|
||||
clearActiveFilter,
|
||||
applyTempFilter,
|
||||
clearTempFilter,
|
||||
saveFilter,
|
||||
updateFilter,
|
||||
deleteFilter,
|
||||
pinFilter,
|
||||
createFilterFromCurrentState,
|
||||
hasProjectConditions,
|
||||
hasFilterApplied,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,14 @@ import {
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { Add, FilterAlt, Star, StarBorder, Task } from '@mui/icons-material'
|
||||
import {
|
||||
Add,
|
||||
FilterAlt,
|
||||
MoreVert,
|
||||
Star,
|
||||
StarBorder,
|
||||
Task,
|
||||
} from '@mui/icons-material'
|
||||
import { useChores } from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import {
|
||||
@@ -328,7 +335,7 @@ const FilterCard = ({
|
||||
return
|
||||
}
|
||||
// Navigate to MyChores with filter applied via URL param
|
||||
navigate(`/chores?filterId=${encodeURIComponent(filter.id)}`)
|
||||
navigate(`/filters/${encodeURIComponent(filter.id)}`)
|
||||
}}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
@@ -362,6 +369,11 @@ const FilterCard = ({
|
||||
}}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
const clampedDelta = isSwipeRevealed ? 0 : -maxSwipeDistance
|
||||
setSwipeTranslateX(clampedDelta)
|
||||
}}
|
||||
>
|
||||
{/* Drag indicator dots */}
|
||||
<Box
|
||||
@@ -371,17 +383,7 @@ const FilterCard = ({
|
||||
gap: 0.25,
|
||||
}}
|
||||
>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
sx={{
|
||||
width: 3,
|
||||
height: 3,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'text.tertiary',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<MoreVert sx={{ fontSize: 20 }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -173,6 +173,12 @@ const AdvancedFilterBuilder = ({
|
||||
}
|
||||
placeholder='Select assignees'
|
||||
sx={{ width: '100%' }}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
placement: 'bottom-start',
|
||||
disablePortal: false,
|
||||
},
|
||||
}}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{selected.map((selectedElement, idx) => {
|
||||
@@ -210,6 +216,12 @@ const AdvancedFilterBuilder = ({
|
||||
}
|
||||
placeholder='Select creators'
|
||||
sx={{ width: '100%' }}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
placement: 'bottom-start',
|
||||
disablePortal: false,
|
||||
},
|
||||
}}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{selected.map((selectedElement, idx) => {
|
||||
@@ -255,13 +267,23 @@ const AdvancedFilterBuilder = ({
|
||||
}
|
||||
placeholder='Select priorities'
|
||||
sx={{ width: '100%' }}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
placement: 'bottom-start',
|
||||
disablePortal: false,
|
||||
},
|
||||
}}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{selected.map((value, idx) => (
|
||||
<Chip key={`priority-${value}-${idx}`} size='sm'>
|
||||
Priority {value}
|
||||
</Chip>
|
||||
))}
|
||||
{selected.map((selectedElement, idx) => {
|
||||
const value = selectedElement.value
|
||||
const priority = Priorities.find(p => p.value === value)
|
||||
return (
|
||||
<Chip key={`priority-${value}-${idx}`} size='sm'>
|
||||
{priority?.name || `Priority ${value}`}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
@@ -286,6 +308,12 @@ const AdvancedFilterBuilder = ({
|
||||
}
|
||||
placeholder='Select labels'
|
||||
sx={{ width: '100%' }}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
placement: 'bottom-start',
|
||||
disablePortal: false,
|
||||
},
|
||||
}}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{selected.map((selectedElement, idx) => {
|
||||
@@ -319,6 +347,12 @@ const AdvancedFilterBuilder = ({
|
||||
}
|
||||
placeholder='Select projects'
|
||||
sx={{ width: '100%' }}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
placement: 'bottom-start',
|
||||
disablePortal: false,
|
||||
},
|
||||
}}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{selected.map((event, idx) => {
|
||||
@@ -363,6 +397,12 @@ const AdvancedFilterBuilder = ({
|
||||
updateCondition(index, 'value', [newValue])
|
||||
}
|
||||
sx={{ width: '100%' }}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
placement: 'bottom-start',
|
||||
disablePortal: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Option value={0}>Active</Option>
|
||||
<Option value={1}>Started</Option>
|
||||
@@ -379,6 +419,12 @@ const AdvancedFilterBuilder = ({
|
||||
updateCondition(index, 'operator', newValue)
|
||||
}
|
||||
sx={{ width: '100%' }}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
placement: 'bottom-start',
|
||||
disablePortal: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Option value='isOverdue'>Is Overdue</Option>
|
||||
<Option value='isDueToday'>Is Due Today</Option>
|
||||
@@ -467,6 +513,12 @@ const AdvancedFilterBuilder = ({
|
||||
<Select
|
||||
value={filterColor}
|
||||
onChange={(_, value) => value && setFilterColor(value)}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
placement: 'bottom-start',
|
||||
disablePortal: false,
|
||||
},
|
||||
}}
|
||||
renderValue={selected => (
|
||||
<Typography
|
||||
startDecorator={
|
||||
@@ -518,8 +570,10 @@ const AdvancedFilterBuilder = ({
|
||||
sx={{
|
||||
gap: 1,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
maxHeight: { xs: '40vh', sm: '50vh' },
|
||||
pr: 0.5,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{conditions.map((condition, index) => (
|
||||
@@ -567,6 +621,12 @@ const AdvancedFilterBuilder = ({
|
||||
updateCondition(index, 'type', newValue)
|
||||
}
|
||||
sx={{ width: '100%' }}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
placement: 'bottom-start',
|
||||
disablePortal: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Option value='assignee'>Assignee</Option>
|
||||
<Option value='createdBy'>Created By</Option>
|
||||
@@ -625,9 +685,11 @@ const AdvancedFilterBuilder = ({
|
||||
sx={{
|
||||
maxHeight: 150,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
bgcolor: 'background.level1',
|
||||
p: 1,
|
||||
borderRadius: 'sm',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{previewCount === 0 ? (
|
||||
|
||||
@@ -24,9 +24,9 @@ import LABEL_COLORS, {
|
||||
import { DeleteProject } from '../../utils/Fetcher'
|
||||
import { getIconComponent } from '../../utils/ProjectIcons'
|
||||
import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
|
||||
import { useProjectFilter } from '../Chores/hooks/useProjectFilter'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import { useProjects } from './ProjectQueries'
|
||||
|
||||
const ProjectCard = ({
|
||||
project,
|
||||
onEditClick,
|
||||
@@ -36,7 +36,9 @@ const ProjectCard = ({
|
||||
taskCounts = {},
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const { data: projects = [], isLoading: projectsLoading } = useProjects()
|
||||
// Helper function to get color name from hex value
|
||||
const { setSelectedProjectWithCache } = useProjectFilter(projects)
|
||||
const getColorName = hexValue => {
|
||||
const colorObj = LABEL_COLORS.find(
|
||||
color => color.value.toLowerCase() === hexValue.toLowerCase(),
|
||||
@@ -311,6 +313,7 @@ const ProjectCard = ({
|
||||
// For default project, use 'default', for others use project ID
|
||||
const projectIdentifier =
|
||||
project.id === 'default' ? 'default' : project.id
|
||||
setSelectedProjectWithCache(project)
|
||||
navigate(`/chores?project=${encodeURIComponent(projectIdentifier)}`)
|
||||
}}
|
||||
onTouchStart={isEditable ? handleTouchStart : undefined}
|
||||
|
||||
Reference in New Issue
Block a user