feat: add custom filter chips and filter section components
- Implemented CustomFilterChips component for displaying and managing custom filters with context menu options for pinning, editing, and deleting filters. - Created FilterSection component to integrate CustomFilterChips and provide UI for creating advanced filters and displaying active filters. - Introduced useCustomFilters hook to manage custom filter logic, including loading, saving, updating, and deleting filters. - Added AdvancedFilterBuilder modal for creating and editing advanced filters with dynamic condition handling. - Implemented SaveFilterModal for saving filters with preview functionality and validation for filter names. - Enhanced useChoreFilters and useProjectFilter hooks for improved filter management and state handling.
This commit is contained in:
594
src/views/Modals/Inputs/AdvancedFilterBuilder.jsx
Normal file
594
src/views/Modals/Inputs/AdvancedFilterBuilder.jsx
Normal file
@@ -0,0 +1,594 @@
|
||||
import { Add, Delete, Save } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
IconButton,
|
||||
Input,
|
||||
List,
|
||||
ListItem,
|
||||
Option,
|
||||
Select,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { filterNameExists } from '../../../utils/CustomFilterStorage'
|
||||
import { applyFilter } from '../../../utils/FilterEngine'
|
||||
import Priorities from '../../../utils/Priorities'
|
||||
|
||||
const AdvancedFilterBuilder = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
members = [],
|
||||
labels = [],
|
||||
projects = [],
|
||||
allChores = [],
|
||||
userProfile = null,
|
||||
editingFilter = null,
|
||||
}) => {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const [filterName, setFilterName] = useState('')
|
||||
const [conditions, setConditions] = useState([
|
||||
{ type: 'assignee', operator: 'is', value: [] },
|
||||
])
|
||||
const [error, setError] = useState('')
|
||||
|
||||
// Initialize state when editing a filter
|
||||
useEffect(() => {
|
||||
if (editingFilter) {
|
||||
setFilterName(editingFilter.name)
|
||||
setConditions(editingFilter.conditions || [])
|
||||
setError('')
|
||||
} else {
|
||||
setFilterName('')
|
||||
setConditions([{ type: 'assignee', operator: 'is', value: [] }])
|
||||
setError('')
|
||||
}
|
||||
}, [editingFilter, isOpen])
|
||||
|
||||
const previewChores = useMemo(() => {
|
||||
const validConditions = conditions.filter(c => {
|
||||
if (c.type === 'dueDate') return true
|
||||
return c.value && (Array.isArray(c.value) ? c.value.length > 0 : true)
|
||||
})
|
||||
|
||||
if (validConditions.length === 0) return []
|
||||
|
||||
const result = applyFilter(
|
||||
allChores,
|
||||
{ conditions: validConditions, operator: 'AND' },
|
||||
{
|
||||
userId: userProfile?.id,
|
||||
members,
|
||||
labels,
|
||||
projects,
|
||||
},
|
||||
)
|
||||
|
||||
return result
|
||||
}, [conditions, allChores, userProfile, members, labels, projects])
|
||||
|
||||
const previewCount = previewChores.length
|
||||
const previewOverdueCount = previewChores.filter(
|
||||
chore => chore.nextDueDate && new Date(chore.nextDueDate) < new Date(),
|
||||
).length
|
||||
|
||||
const addCondition = () => {
|
||||
setConditions([
|
||||
...conditions,
|
||||
{ type: 'assignee', operator: 'is', value: [] },
|
||||
])
|
||||
}
|
||||
|
||||
const removeCondition = index => {
|
||||
setConditions(conditions.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const updateCondition = (index, field, value) => {
|
||||
const updated = [...conditions]
|
||||
updated[index] = { ...updated[index], [field]: value }
|
||||
|
||||
if (field === 'type') {
|
||||
updated[index].value = []
|
||||
if (value === 'dueDate') {
|
||||
updated[index].operator = 'isOverdue'
|
||||
updated[index].value = null
|
||||
} else if (value === 'status') {
|
||||
updated[index].value = [3]
|
||||
}
|
||||
}
|
||||
|
||||
setConditions(updated)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!filterName.trim()) {
|
||||
setError('Please enter a filter name')
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicate name, excluding current filter if editing
|
||||
if (filterNameExists(filterName.trim(), editingFilter?.id)) {
|
||||
setError('A filter with this name already exists')
|
||||
return
|
||||
}
|
||||
|
||||
const validConditions = conditions.filter(c => {
|
||||
if (c.type === 'dueDate') return true
|
||||
return c.value && (Array.isArray(c.value) ? c.value.length > 0 : true)
|
||||
})
|
||||
|
||||
if (conditions.length === 0 || validConditions.length === 0) {
|
||||
setError('Please add at least one filter condition')
|
||||
return
|
||||
}
|
||||
|
||||
const filterData = {
|
||||
name: filterName.trim(),
|
||||
conditions: validConditions,
|
||||
operator: 'AND',
|
||||
}
|
||||
|
||||
// Include ID if editing
|
||||
if (editingFilter) {
|
||||
filterData.id = editingFilter.id
|
||||
}
|
||||
|
||||
onSave(filterData)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const renderValueSelector = (condition, index) => {
|
||||
switch (condition.type) {
|
||||
case 'assignee':
|
||||
return (
|
||||
<Select
|
||||
multiple
|
||||
value={condition.value || []}
|
||||
onChange={(_, newValue) =>
|
||||
updateCondition(index, 'value', newValue)
|
||||
}
|
||||
placeholder='Select assignees'
|
||||
sx={{ width: '100%' }}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{selected.map((selectedElement, idx) => {
|
||||
const value = selectedElement.value
|
||||
const member = members.find(
|
||||
m => String(m.userId) === String(value),
|
||||
)
|
||||
return (
|
||||
<Chip key={`${value}-${idx}`} size='sm'>
|
||||
{member?.displayName || member?.username || 'Unknown'}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
{members.map((member, idx) => (
|
||||
<Option
|
||||
key={`member-${member.userId}-${idx}`}
|
||||
value={member.userId}
|
||||
>
|
||||
{member.displayName || member.username} ({member.userId})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
|
||||
case 'createdBy':
|
||||
return (
|
||||
<Select
|
||||
multiple
|
||||
value={condition.value || []}
|
||||
onChange={(_, newValue) =>
|
||||
updateCondition(index, 'value', newValue)
|
||||
}
|
||||
placeholder='Select creators'
|
||||
sx={{ width: '100%' }}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{selected.map((selectedElement, idx) => {
|
||||
const value = selectedElement.value
|
||||
|
||||
if (value === 'me')
|
||||
return (
|
||||
<Chip key={`${value}-${idx}`} size='sm'>
|
||||
Me
|
||||
</Chip>
|
||||
)
|
||||
const member = members.find(
|
||||
m => String(m.userId) === String(value),
|
||||
)
|
||||
return (
|
||||
<Chip key={`${value}-${idx}`} size='sm'>
|
||||
{member?.displayName || member?.username || 'Unknown'}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Option value='me'>Me</Option>
|
||||
{members.map((member, idx) => (
|
||||
<Option
|
||||
key={`creator-${member.userId}-${idx}`}
|
||||
value={member.userId}
|
||||
>
|
||||
{member.displayName || member.username}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
|
||||
case 'priority':
|
||||
return (
|
||||
<Select
|
||||
multiple
|
||||
value={condition.value || []}
|
||||
onChange={(_, newValue) =>
|
||||
updateCondition(index, 'value', newValue)
|
||||
}
|
||||
placeholder='Select priorities'
|
||||
sx={{ width: '100%' }}
|
||||
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>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
{Priorities.map((priority, idx) => (
|
||||
<Option
|
||||
key={`priority-opt-${priority.value}-${idx}`}
|
||||
value={priority.value}
|
||||
>
|
||||
{priority.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
|
||||
case 'label':
|
||||
return (
|
||||
<Select
|
||||
multiple
|
||||
value={condition.value || []}
|
||||
onChange={(_, newValue) =>
|
||||
updateCondition(index, 'value', newValue)
|
||||
}
|
||||
placeholder='Select labels'
|
||||
sx={{ width: '100%' }}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{selected.map((selectedElement, idx) => {
|
||||
const value = selectedElement.value
|
||||
|
||||
const label = labels.find(l => String(l.id) === String(value))
|
||||
return (
|
||||
<Chip key={`label-chip-${value}-${idx}`} size='sm'>
|
||||
{label?.name || 'Unknown'}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
{labels.map((label, idx) => (
|
||||
<Option key={`label-opt-${label.id}-${idx}`} value={label.id}>
|
||||
{label.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
|
||||
case 'project':
|
||||
return (
|
||||
<Select
|
||||
multiple
|
||||
value={condition.value || []}
|
||||
onChange={(_, newValue) =>
|
||||
updateCondition(index, 'value', newValue)
|
||||
}
|
||||
placeholder='Select projects'
|
||||
sx={{ width: '100%' }}
|
||||
renderValue={selected => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{selected.map((event, idx) => {
|
||||
const value = event.value
|
||||
if (value === 'default')
|
||||
return (
|
||||
<Chip key={`default-${idx}`} size='sm'>
|
||||
Default
|
||||
</Chip>
|
||||
)
|
||||
const project = projects.find(
|
||||
p => String(p.id) === String(value),
|
||||
)
|
||||
return (
|
||||
<Chip key={`project-chip-${value}-${idx}`} size='sm'>
|
||||
{project?.name || 'Unknown'}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Option value='default'>Default Project</Option>
|
||||
{projects
|
||||
.filter(p => p.id !== 'default')
|
||||
.map((project, idx) => (
|
||||
<Option
|
||||
key={`project-opt-${project.id}-${idx}`}
|
||||
value={project.id}
|
||||
>
|
||||
{project.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
|
||||
case 'status':
|
||||
return (
|
||||
<Select
|
||||
value={condition.value?.[0] || 3}
|
||||
onChange={(_, newValue) =>
|
||||
updateCondition(index, 'value', [newValue])
|
||||
}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<Option value={0}>Active</Option>
|
||||
<Option value={1}>Started</Option>
|
||||
<Option value={2}>In Progress</Option>
|
||||
<Option value={3}>Pending Approval</Option>
|
||||
</Select>
|
||||
)
|
||||
|
||||
case 'dueDate':
|
||||
return (
|
||||
<Select
|
||||
value={condition.operator}
|
||||
onChange={(_, newValue) =>
|
||||
updateCondition(index, 'operator', newValue)
|
||||
}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<Option value='isOverdue'>Is Overdue</Option>
|
||||
<Option value='isDueToday'>Is Due Today</Option>
|
||||
<Option value='isDueTomorrow'>Is Due Tomorrow</Option>
|
||||
<Option value='isDueThisWeek'>Is Due This Week</Option>
|
||||
<Option value='isDueThisMonth'>Is Due This Month</Option>
|
||||
<Option value='hasNoDueDate'>Has No Due Date</Option>
|
||||
<Option value='hasDueDate'>Has Due Date</Option>
|
||||
</Select>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveModal open={isOpen} onClose={onClose} size='md'>
|
||||
<Typography level='h4' sx={{ mb: 2 }}>
|
||||
{editingFilter ? 'Edit Filter' : 'Create Advanced Filter'}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||
Filter Name
|
||||
</Typography>
|
||||
<Input
|
||||
placeholder='e.g., High Priority Tasks for Team'
|
||||
value={filterName}
|
||||
onChange={e => {
|
||||
setFilterName(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
error={!!error}
|
||||
autoFocus
|
||||
/>
|
||||
{error && (
|
||||
<Typography level='body-sm' color='danger' sx={{ mt: 0.5 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||
Filter Conditions (All must match)
|
||||
</Typography>
|
||||
|
||||
<List
|
||||
sx={{
|
||||
gap: 1,
|
||||
overflowY: 'auto',
|
||||
maxHeight: { xs: '40vh', sm: '50vh' },
|
||||
pr: 0.5,
|
||||
}}
|
||||
>
|
||||
{conditions.map((condition, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
bgcolor: 'background.level1',
|
||||
borderRadius: 'sm',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Condition {index + 1}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size='sm'
|
||||
color='danger'
|
||||
variant='plain'
|
||||
onClick={() => removeCondition(index)}
|
||||
disabled={conditions.length === 1}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography level='body-xs' sx={{ mb: 0.5 }}>
|
||||
Field
|
||||
</Typography>
|
||||
<Select
|
||||
value={condition.type}
|
||||
onChange={(_, newValue) =>
|
||||
updateCondition(index, 'type', newValue)
|
||||
}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<Option value='assignee'>Assignee</Option>
|
||||
<Option value='createdBy'>Created By</Option>
|
||||
<Option value='priority'>Priority</Option>
|
||||
<Option value='label'>Label</Option>
|
||||
<Option value='project'>Project</Option>
|
||||
<Option value='status'>Status</Option>
|
||||
<Option value='dueDate'>Due Date</Option>
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography level='body-xs' sx={{ mb: 0.5 }}>
|
||||
{condition.type === 'dueDate' ? 'Condition' : 'Value'}
|
||||
</Typography>
|
||||
{renderValueSelector(condition, index)}
|
||||
</Box>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
startDecorator={<Add />}
|
||||
onClick={addCondition}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm'>Preview</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Chip size='sm' variant='soft' color='neutral'>
|
||||
{previewCount} tasks
|
||||
</Chip>
|
||||
{previewOverdueCount > 0 && (
|
||||
<Chip size='sm' variant='solid' color='danger'>
|
||||
{previewOverdueCount} overdue
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: 150,
|
||||
overflowY: 'auto',
|
||||
bgcolor: 'background.level1',
|
||||
p: 1,
|
||||
borderRadius: 'sm',
|
||||
}}
|
||||
>
|
||||
{previewCount === 0 ? (
|
||||
<Typography
|
||||
level='body-sm'
|
||||
color='neutral'
|
||||
sx={{ textAlign: 'center', py: 2 }}
|
||||
>
|
||||
No tasks match these filters
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{previewChores.slice(0, 3).map(chore => (
|
||||
<Box
|
||||
key={chore.id}
|
||||
sx={{
|
||||
bgcolor: 'background.surface',
|
||||
p: 1,
|
||||
borderRadius: 'sm',
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm'>{chore.name}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
{previewCount > 3 && (
|
||||
<Typography
|
||||
level='body-xs'
|
||||
color='neutral'
|
||||
sx={{ textAlign: 'center', mt: 0.5 }}
|
||||
>
|
||||
...and {previewCount - 3} more
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
||||
<Button variant='outlined' color='neutral' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='solid'
|
||||
color='primary'
|
||||
onClick={handleSave}
|
||||
startDecorator={<Save />}
|
||||
>
|
||||
{editingFilter ? 'Update Filter' : 'Save Filter'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdvancedFilterBuilder
|
||||
263
src/views/Modals/Inputs/SaveFilterModal.jsx
Normal file
263
src/views/Modals/Inputs/SaveFilterModal.jsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import { Save, Star, StarBorder } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Input,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalDialog,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { filterNameExists } from '../../../utils/CustomFilterStorage'
|
||||
import CompactChoreCard from '../../Chores/CompactChoreCard'
|
||||
|
||||
const SaveFilterModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
filterData,
|
||||
previewChores = [],
|
||||
previewCount = 0,
|
||||
previewOverdueCount = 0,
|
||||
}) => {
|
||||
const [filterName, setFilterName] = useState('')
|
||||
const [isPinned, setIsPinned] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSave = () => {
|
||||
if (!filterName.trim()) {
|
||||
setError('Please enter a filter name')
|
||||
return
|
||||
}
|
||||
|
||||
if (filterNameExists(filterName.trim())) {
|
||||
setError('A filter with this name already exists')
|
||||
return
|
||||
}
|
||||
|
||||
const newFilter = {
|
||||
...filterData,
|
||||
name: filterName.trim(),
|
||||
isPinned,
|
||||
}
|
||||
|
||||
onSave(newFilter)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const getConditionLabel = condition => {
|
||||
switch (condition.type) {
|
||||
case 'assignee':
|
||||
if (condition.value === 'me') return 'Assigned to me'
|
||||
if (condition.value === 'others') return 'Assigned to others'
|
||||
return 'Specific assignee'
|
||||
|
||||
case 'createdBy':
|
||||
if (condition.value === 'me') return 'Created by me'
|
||||
return 'Created by specific user'
|
||||
|
||||
case 'priority':
|
||||
return `Priority ${condition.value}`
|
||||
|
||||
case 'status':
|
||||
return condition.value === 3 ? 'Pending approval' : `Status ${condition.value}`
|
||||
|
||||
case 'dueDate':
|
||||
if (condition.operator === 'isOverdue') return 'Overdue'
|
||||
if (condition.operator === 'isDueToday') return 'Due today'
|
||||
if (condition.operator === 'isDueThisWeek') return 'Due this week'
|
||||
if (condition.operator === 'hasNoDueDate') return 'No due date'
|
||||
return 'Due date condition'
|
||||
|
||||
case 'label':
|
||||
return 'Has label'
|
||||
|
||||
case 'project':
|
||||
if (condition.value === 'default') return 'Default project'
|
||||
return 'Specific project'
|
||||
|
||||
default:
|
||||
return condition.type
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onClose={onClose}>
|
||||
<ModalDialog
|
||||
sx={{
|
||||
maxWidth: 500,
|
||||
width: '90%',
|
||||
maxHeight: '90vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<ModalClose />
|
||||
<Typography level='h4' sx={{ mb: 2 }}>
|
||||
Save Filter
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||
Filter Name
|
||||
</Typography>
|
||||
<Input
|
||||
placeholder='e.g., My High Priority Tasks'
|
||||
value={filterName}
|
||||
onChange={e => {
|
||||
setFilterName(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
error={!!error}
|
||||
autoFocus
|
||||
/>
|
||||
{error && (
|
||||
<Typography level='body-sm' color='danger' sx={{ mt: 0.5 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||
Filter Conditions
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
{filterData.conditions.length === 0 ? (
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
No filters applied
|
||||
</Typography>
|
||||
) : (
|
||||
filterData.conditions.map((condition, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
>
|
||||
{getConditionLabel(condition)}
|
||||
</Chip>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm'>Preview</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Chip size='sm' variant='soft' color='neutral'>
|
||||
{previewCount} tasks
|
||||
</Chip>
|
||||
{previewOverdueCount > 0 && (
|
||||
<Chip size='sm' variant='solid' color='danger'>
|
||||
{previewOverdueCount} overdue
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: 200,
|
||||
overflowY: 'auto',
|
||||
bgcolor: 'background.level1',
|
||||
p: 1,
|
||||
borderRadius: 'sm',
|
||||
}}
|
||||
>
|
||||
{previewCount === 0 ? (
|
||||
<Typography
|
||||
level='body-sm'
|
||||
color='neutral'
|
||||
sx={{ textAlign: 'center', py: 2 }}
|
||||
>
|
||||
No tasks match these filters
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{previewChores.slice(0, 3).map(chore => (
|
||||
<Box
|
||||
key={chore.id}
|
||||
sx={{
|
||||
bgcolor: 'background.surface',
|
||||
p: 1,
|
||||
borderRadius: 'sm',
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm'>{chore.name}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
{previewCount > 3 && (
|
||||
<Typography
|
||||
level='body-xs'
|
||||
color='neutral'
|
||||
sx={{ textAlign: 'center', mt: 0.5 }}
|
||||
>
|
||||
...and {previewCount - 3} more
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
cursor: 'pointer',
|
||||
p: 1,
|
||||
borderRadius: 'sm',
|
||||
'&:hover': {
|
||||
bgcolor: 'background.level1',
|
||||
},
|
||||
}}
|
||||
onClick={() => setIsPinned(!isPinned)}
|
||||
>
|
||||
{isPinned ? (
|
||||
<Star color='warning' />
|
||||
) : (
|
||||
<StarBorder color='neutral' />
|
||||
)}
|
||||
<Box>
|
||||
<Typography level='body-sm' fontWeight='md'>
|
||||
Pin this filter
|
||||
</Typography>
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Pinned filters appear first in the list
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
||||
<Button variant='outlined' color='neutral' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='solid'
|
||||
color='primary'
|
||||
onClick={handleSave}
|
||||
startDecorator={<Save />}
|
||||
disabled={!filterName.trim() || filterData.conditions.length === 0}
|
||||
>
|
||||
Save Filter
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SaveFilterModal
|
||||
Reference in New Issue
Block a user