Add multiselect.

This commit is contained in:
Drew 2025-09-22 21:21:30 -07:00
parent 843d2a8c7b
commit b09a072fb9
7 changed files with 235 additions and 21 deletions

View file

@ -17,9 +17,18 @@ import {
TableRow,
useMediaQuery,
useTheme,
Checkbox,
Toolbar,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
} from '@mui/material'
import { Delete as DeleteIcon } from '@mui/icons-material'
import { useTasks } from '~/hooks/useTasks'
import { TaskStatus } from '~/types/task'
import { TaskStatus, type Task } from '~/types/task'
import LoadingSpinner from './LoadingSpinner'
type SortOption =
@ -31,12 +40,18 @@ type SortOption =
interface TaskListProps {
className?: string
initialTasks?: Task[]
}
export function TaskList({ className }: TaskListProps) {
const { tasks, loading, error } = useTasks()
export function TaskList({ className, initialTasks }: TaskListProps) {
const { tasks, loading, error, deleteTask } = useTasks({
autoFetch: !initialTasks,
initialData: initialTasks,
})
const [statusFilter, setStatusFilter] = useState<TaskStatus | 'all'>('all')
const [sortBy, setSortBy] = useState<SortOption>('created_desc')
const [selectedTaskIds, setSelectedTaskIds] = useState<Set<string>>(new Set())
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
@ -107,6 +122,54 @@ export function TaskList({ className }: TaskListProps) {
return `${Math.floor(diffInDays / 365)}y ago`
}
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedTaskIds(new Set(filteredAndSortedTasks.map(task => task.id)))
} else {
setSelectedTaskIds(new Set())
}
}
const handleSelectTask = (taskId: string, checked: boolean) => {
const newSelected = new Set(selectedTaskIds)
if (checked) {
newSelected.add(taskId)
} else {
newSelected.delete(taskId)
}
setSelectedTaskIds(newSelected)
}
const handleBulkDelete = async () => {
try {
const results = await Promise.all(
Array.from(selectedTaskIds).map(taskId => deleteTask(taskId))
)
// Check if all deletions were successful
const allSuccessful = results.every(result => result === true)
if (allSuccessful) {
setSelectedTaskIds(new Set())
setDeleteDialogOpen(false)
} else {
console.error('Some tasks failed to delete')
// Clear selection of successfully deleted tasks
const failedTaskIds = Array.from(selectedTaskIds).filter(
(taskId, index) => !results[index]
)
setSelectedTaskIds(new Set(failedTaskIds))
}
} catch (error) {
console.error('Error deleting tasks:', error)
}
}
const isAllSelected =
filteredAndSortedTasks.length > 0 &&
filteredAndSortedTasks.every(task => selectedTaskIds.has(task.id))
const isIndeterminate = selectedTaskIds.size > 0 && !isAllSelected
if (loading) {
return <LoadingSpinner />
}
@ -171,6 +234,31 @@ export function TaskList({ className }: TaskListProps) {
</Stack>
</Paper>
{/* Bulk Actions Toolbar */}
{selectedTaskIds.size > 0 && (
<Paper sx={{ mb: 2 }}>
<Toolbar sx={{ pl: 2, pr: 1 }}>
<Typography
sx={{ flex: '1 1 100%' }}
color="inherit"
variant="subtitle1"
component="div"
>
{selectedTaskIds.size} task{selectedTaskIds.size > 1 ? 's' : ''}{' '}
selected
</Typography>
<Button
variant="contained"
color="error"
startIcon={<DeleteIcon />}
onClick={() => setDeleteDialogOpen(true)}
>
Delete
</Button>
</Toolbar>
</Paper>
)}
{/* Task Table */}
{filteredAndSortedTasks.length === 0 ? (
<Paper sx={{ p: 4, textAlign: 'center' }}>
@ -188,6 +276,17 @@ export function TaskList({ className }: TaskListProps) {
<Table size="small" sx={{ '& .MuiTableCell-root': { py: 1 } }}>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
color="primary"
indeterminate={isIndeterminate}
checked={isAllSelected}
onChange={e => handleSelectAll(e.target.checked)}
inputProps={{
'aria-label': 'select all tasks',
}}
/>
</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Title</TableCell>
{!isMobile && (
<TableCell sx={{ fontWeight: 'bold' }}>Description</TableCell>
@ -214,11 +313,27 @@ export function TaskList({ className }: TaskListProps) {
backgroundColor: 'action.hover',
},
'&:last-child td, &:last-child th': { border: 0 },
backgroundColor: selectedTaskIds.has(task.id)
? 'action.selected'
: 'inherit',
}}
>
<TableCell padding="checkbox">
<Checkbox
color="primary"
checked={selectedTaskIds.has(task.id)}
onChange={e =>
handleSelectTask(task.id, e.target.checked)
}
inputProps={{
'aria-labelledby': `task-${task.id}`,
}}
/>
</TableCell>
<TableCell>
<Box>
<Typography
id={`task-${task.id}`}
variant="body1"
component="div"
sx={{
@ -287,6 +402,31 @@ export function TaskList({ className }: TaskListProps) {
</Table>
</TableContainer>
)}
{/* Delete Confirmation Dialog */}
<Dialog
open={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
aria-labelledby="delete-dialog-title"
aria-describedby="delete-dialog-description"
>
<DialogTitle id="delete-dialog-title">
Delete {selectedTaskIds.size} task
{selectedTaskIds.size > 1 ? 's' : ''}?
</DialogTitle>
<DialogContent>
<DialogContentText id="delete-dialog-description">
This action cannot be undone. Are you sure you want to delete the
selected task{selectedTaskIds.size > 1 ? 's' : ''}?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
<Button onClick={handleBulkDelete} color="error" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
</Box>
)
}