diff --git a/src/components/common/filter/ActiveFilterChips.jsx b/src/components/common/filter/ActiveFilterChips.jsx index 4e06220..b9f9049 100644 --- a/src/components/common/filter/ActiveFilterChips.jsx +++ b/src/components/common/filter/ActiveFilterChips.jsx @@ -1,10 +1,12 @@ -import { Close } from '@mui/icons-material' -import { Box, Button, Chip, Typography } from '@mui/joy' +import { Add, Close } from '@mui/icons-material' +import { Box, Button, Chip, ChipDelete, Typography } from '@mui/joy' const ActiveFilterChips = ({ chips = [], onOpen, onClearAll, + onAdd, + showAddChip = false, resultCount, totalCount, maxVisible = 2, @@ -44,13 +46,21 @@ const ActiveFilterChips = ({ variant='soft' color={color} endDecorator={ - { - e.stopPropagation() - onClear?.() + // ChipDelete rather than a bare icon: Joy's chip end decorator is + // `pointer-events: none`, so anything else here is swallowed by the + // chip's own click surface and can never clear the condition. + onClear?.()} + aria-label={`Remove ${label} filter`} + sx={{ + '--Chip-deleteSize': chipSize === 'sm' ? '1.1rem' : '1.4rem', + '--Icon-fontSize': chipSize === 'sm' ? '12px' : '16px', }} - /> + > + + } onClick={onOpen} sx={{ @@ -74,7 +84,10 @@ const ActiveFilterChips = ({ ))} - {overflow > 0 && ( + {/* Everything already visible → spend that slot on a "+" for appending + another condition instead. With overflow, the count chip opens the + same sheet anyway. */} + {overflow > 0 ? ( +{overflow} more + ) : ( + showAddChip && + (onAdd || onOpen) && ( + + + + ) )} {resultCount != null && totalCount != null && ( diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index 0fd0185..fd6afac 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -52,6 +52,10 @@ import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder' import { useProjects } from '../Projects/ProjectQueries.js' import ChoreListView from './ChoreListView.jsx' import ChoreToolbar from './components/ChoreToolbarPrototype' +import { + conditionsToSelections, + selectionsToConditions, +} from './components/FilterBuilderContent' import ChoreModals from './components/ChoreModals' import MultiSelectToolbar from './components/MultiSelectToolbar' import MyChoreHeader from './components/MyChoreHeader' @@ -683,14 +687,46 @@ const MyChores = () => { setAnchorEl(null) } + // Clicking a label / priority chip on a task card feeds the advanced filter + // (as a temp filter) rather than the legacy quick filters. Clicking the same + // chip again removes that value, so chips toggle. const handleLabelFiltering = chipClicked => { - clearActiveFilter() - if (chipClicked.label) { - setQuickFilter('label', [chipClicked.label.id]) - } else if (chipClicked.priority) { - setQuickFilter('priority', [chipClicked.priority]) + const type = chipClicked.label ? 'label' : 'priority' + const value = chipClicked.label + ? chipClicked.label.id + : chipClicked.priority + if (value === undefined || value === null) return + + const selections = conditionsToSelections(tempFilter?.conditions) + const currentValues = selections[type].values || [] + const isActive = currentValues.some(v => String(v) === String(value)) + selections[type] = { + operator: selections[type].operator || 'is', + values: isActive + ? currentValues.filter(v => String(v) !== String(value)) + : [...currentValues, value], } + + const conditions = selectionsToConditions(selections) + + clearQuickFilters() setSelectedCalendarDate(null) + + if (conditions.length === 0) { + clearTempFilter() + clearActiveFilter() + return + } + + const chipName = chipClicked.label + ? chipClicked.label.name + : `P${chipClicked.priority}` + applyTempFilter( + { conditions, operator: 'AND' }, + // Keep whatever the temp filter was already labelled as (e.g. an + // in-progress saved-filter edit); only name it when starting fresh. + tempFilterMeta ?? { name: chipName }, + ) } // Helper to update URL with filter parameters diff --git a/src/views/Chores/components/ChoreToolbarPrototype.jsx b/src/views/Chores/components/ChoreToolbarPrototype.jsx index 986884a..29960b4 100644 --- a/src/views/Chores/components/ChoreToolbarPrototype.jsx +++ b/src/views/Chores/components/ChoreToolbarPrototype.jsx @@ -317,27 +317,6 @@ const ChoreToolbar = ({ return `${prefix}${typeLabel} (${rawValues.length})` } - const clearConditionAtIndex = index => { - const nextConditions = (tempFilter?.conditions || []).filter( - (_condition, conditionIndex) => conditionIndex !== index, - ) - - if (nextConditions.length === 0) { - setLocalSelections(defaultSelections()) - clearTempFilter?.() - return - } - - const nextFilter = { - ...tempFilter, - operator: tempFilter?.operator || 'AND', - conditions: nextConditions, - } - - setLocalSelections(conditionsToSelections(nextConditions)) - applyTempFilter?.(nextFilter) - } - const activeSavedFilter = savedFilterActive ? savedFilters.find(f => f.id === activeFilterId) : null @@ -346,17 +325,61 @@ const ChoreToolbar = ({ ? activeSavedFilter?.conditions || [] : tempFilter?.conditions || [] + const savedFilterEditMeta = activeSavedFilter + ? { + name: activeSavedFilter.name, + description: activeSavedFilter.description, + sourceFilterId: activeSavedFilter.id, + sourceFilterName: activeSavedFilter.name, + sourceFilterDescription: activeSavedFilter.description, + sourceFilterColor: activeSavedFilter.color, + isEditingSavedFilter: true, + } + : null + + const clearConditionAtIndex = index => { + const nextConditions = activeChipConditions.filter( + (_condition, conditionIndex) => conditionIndex !== index, + ) + + if (nextConditions.length === 0) { + setLocalSelections(defaultSelections()) + clearTempFilter?.() + // A saved filter still owns the view until it's toggled back off. + if (savedFilterActive) onSavedFilterClick?.(activeFilterId) + return + } + + setLocalSelections(conditionsToSelections(nextConditions)) + + // Dropping a condition from a saved filter detaches it into a temp filter + // that remembers its source, rather than editing the saved filter itself. + if (savedFilterActive) { + applyTempFilter?.( + { + conditions: nextConditions, + operator: activeSavedFilter?.operator || 'AND', + }, + savedFilterEditMeta, + ) + return + } + + applyTempFilter?.( + { + ...tempFilter, + operator: tempFilter?.operator || 'AND', + conditions: nextConditions, + }, + tempFilterMeta, + ) + } + activeChipConditions.forEach((condition, index) => { inlineChips.push({ key: `${savedFilterActive ? '__saved' : '__temp'}_${index}`, label: getConditionChipLabel(condition), - onClear: () => { - if (savedFilterActive) { - onSavedFilterClick?.(activeFilterId) - return - } - clearConditionAtIndex(index) - }, + onClear: () => clearConditionAtIndex(index), }) }) @@ -638,6 +661,8 @@ const ChoreToolbar = ({ { setLocalSelections(defaultSelections()) onClearAllFilters?.() diff --git a/src/views/components/ScanToTask/ScanPanel.jsx b/src/views/components/ScanToTask/ScanPanel.jsx index 3cf41a8..dc2c27c 100644 --- a/src/views/components/ScanToTask/ScanPanel.jsx +++ b/src/views/components/ScanToTask/ScanPanel.jsx @@ -138,15 +138,7 @@ const ScanPanel = ({ const isProcessing = phase === 'processing' return ( - + {/* ── Capture phase ── */} {phase === 'capture' && ( <> @@ -160,6 +152,8 @@ const ScanPanel = ({ alignItems: 'center', justifyContent: 'center', bgcolor: 'neutral.900', + // The wrapper used to clip this; it owns its own corners now + borderRadius: 'md', overflow: 'hidden', }} > @@ -211,7 +205,6 @@ const ScanPanel = ({ {(isNativeScanner || cameraAvailable) && ( {capturedImage && ( @@ -297,7 +290,7 @@ const ScanPanel = ({ {/* ── Error phase ── */} {phase === 'error' && ( - + {capturedImage && ( + {/* ── Header ── */} + @@ -532,7 +523,6 @@ const VoicePanel = ({ +