Files
donetick/src/views/components/ChoreActionMenu.jsx
Mo Tarbin eb8c178603 Add ability to move project from choreactionMenu
fix header not syncing on mychoreHeader
2026-07-04 12:00:12 -04:00

531 lines
14 KiB
JavaScript

import {
Archive,
ArrowBack,
Cancel,
CopyAll,
Delete,
DriveFileMove,
Edit,
ManageSearch,
MoreTime,
MoreVert,
NextWeek,
Nfc,
NoteAdd,
Notifications,
RecordVoiceOver,
SwitchAccessShortcut,
Today,
Unarchive,
Update,
ViewCarousel,
WbSunny,
Weekend,
} from '@mui/icons-material'
import {
Avatar,
Divider,
IconButton,
ListItemContent,
ListItemDecorator,
Menu,
MenuItem,
Tooltip,
Typography,
} from '@mui/joy'
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import LABEL_COLORS, {
getTextColorFromBackgroundColor,
} from '../../utils/Colors'
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
import { getIconComponent } from '../../utils/ProjectIcons'
import { useProjects } from '../Projects/ProjectQueries'
const ChoreActionMenu = ({
chore,
onAction,
onCompleteWithNote,
onCompleteWithPastDate,
onChangeAssignee,
onChangeDueDate,
onWriteNFC,
onNudge,
onDelete,
onOpen,
onMouseEnter,
onMouseLeave,
sx = {},
variant = 'soft',
}) => {
const [anchorEl, setAnchorEl] = React.useState(null)
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
const [showProjectPicker, setShowProjectPicker] = useState(false)
const menuRef = React.useRef(null)
const navigate = useNavigate()
const { data: projects = [] } = useProjects()
useEffect(() => {
try {
setIsOfficialInstance(isOfficialDonetickInstanceSync())
} catch (error) {
console.warn('Error checking instance type:', error)
setIsOfficialInstance(false)
}
}, [])
useEffect(() => {
const handleMenuOutsideClick = event => {
if (
anchorEl &&
!anchorEl.contains(event.target) &&
!menuRef.current.contains(event.target)
) {
handleMenuClose()
}
}
document.addEventListener('mousedown', handleMenuOutsideClick)
if (anchorEl && onOpen) {
onOpen()
}
return () => {
document.removeEventListener('mousedown', handleMenuOutsideClick)
}
}, [anchorEl, onOpen])
const handleMenuOpen = event => {
event.stopPropagation()
setAnchorEl(event.currentTarget)
}
const handleMenuClose = () => {
setAnchorEl(null)
setShowProjectPicker(false)
}
const handleMoveToProject = project => {
onAction?.('moveToProject', chore, { project })
handleMenuClose()
}
const handleEdit = () => {
navigate(`/chores/${chore.id}/edit`)
handleMenuClose()
}
const handleClone = () => {
navigate(`/chores/${chore.id}/edit?clone=true`)
handleMenuClose()
}
const handleView = () => {
navigate(`/chores/${chore.id}`)
handleMenuClose()
}
const handleDelete = () => {
if (onDelete) {
onDelete()
} else {
onAction?.('delete', chore)
}
handleMenuClose()
}
const handleArchive = () => {
if (chore.isActive) {
onAction?.('archive', chore)
} else {
onAction?.('unarchive', chore)
}
handleMenuClose()
}
const handleSkip = () => {
onAction?.('skip', chore)
handleMenuClose()
}
const handleHistory = () => {
navigate(`/chores/${chore.id}/history`)
handleMenuClose()
}
const getQuickScheduleDate = option => {
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
switch (option) {
case 'today': {
const nowHour = now.getHours()
const scheduled = new Date(today)
if (nowHour < 9) {
scheduled.setHours(9, 0, 0, 0)
} else if (nowHour < 12) {
scheduled.setHours(12, 0, 0, 0)
} else if (nowHour < 17) {
scheduled.setHours(17, 0, 0, 0)
} else {
scheduled.setHours(
now.getHours(),
now.getMinutes(),
now.getSeconds(),
now.getMilliseconds(),
)
}
return scheduled
}
case 'tomorrow-morning': {
const tomorrowMorning = new Date(today)
tomorrowMorning.setDate(today.getDate() + 1)
tomorrowMorning.setHours(9, 0, 0, 0)
return tomorrowMorning
}
case 'tomorrow': {
const tomorrow = new Date(today)
tomorrow.setDate(today.getDate() + 1)
tomorrow.setHours(12, 0, 0, 0)
return tomorrow
}
case 'tomorrow-afternoon': {
const tomorrowAfternoon = new Date(today)
tomorrowAfternoon.setDate(today.getDate() + 1)
tomorrowAfternoon.setHours(14, 0, 0, 0)
return tomorrowAfternoon
}
case 'weekend': {
const weekend = new Date(today)
const daysUntilSaturday = (6 - today.getDay() + 7) % 7 || 7
weekend.setDate(today.getDate() + daysUntilSaturday)
return weekend
}
case 'next-week': {
const nextWeek = new Date(today)
const daysUntilMonday = (1 - today.getDay() + 7) % 7 || 7
nextWeek.setDate(today.getDate() + daysUntilMonday)
return nextWeek
}
default:
return today
}
}
const handleQuickSchedule = option => {
const date = option === 'remove' ? null : getQuickScheduleDate(option)
onAction?.('changeDueDate', chore, { date })
handleMenuClose()
}
const renderProjectAvatar = (color, icon) => {
const bg = color || LABEL_COLORS[0].value
const IconComponent = getIconComponent(icon || 'FolderOpen')
return (
<Avatar size='sm' sx={{ width: 22, height: 22, backgroundColor: bg }}>
<IconComponent
sx={{ fontSize: 13, color: getTextColorFromBackgroundColor(bg) }}
/>
</Avatar>
)
}
return (
<>
<IconButton
variant={variant}
color='success'
onClick={handleMenuOpen}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
sx={{
borderRadius: '50%',
width: 25,
height: 25,
position: 'relative',
left: -10,
...sx,
}}
>
<MoreVert />
</IconButton>
<Menu
size='md'
ref={menuRef}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
sx={{
position: 'absolute',
top: '100%',
left: '50%',
}}
>
{showProjectPicker ? (
<>
<MenuItem
onClick={e => {
e.stopPropagation()
setShowProjectPicker(false)
}}
sx={{ gap: 1 }}
>
<ArrowBack fontSize='small' />
<Typography level='body-sm' fontWeight={600}>
Move to project
</Typography>
</MenuItem>
<Divider />
<MenuItem
onClick={e => {
e.stopPropagation()
handleMoveToProject({ id: null, name: 'Default Project' })
}}
>
<ListItemDecorator>
{renderProjectAvatar(LABEL_COLORS[0].value, 'FolderOpen')}
</ListItemDecorator>
<ListItemContent>
<Typography level='body-sm'>Default Project</Typography>
</ListItemContent>
</MenuItem>
{projects.map(project => (
<MenuItem
key={project.id}
onClick={e => {
e.stopPropagation()
handleMoveToProject(project)
}}
>
<ListItemDecorator>
{renderProjectAvatar(project.color, project.icon)}
</ListItemDecorator>
<ListItemContent>
<Typography level='body-sm'>{project.name}</Typography>
</ListItemContent>
</MenuItem>
))}
</>
) : (
<>
<MenuItem
onClick={e => {
e.stopPropagation()
onCompleteWithNote?.()
handleMenuClose()
}}
>
<NoteAdd />
Complete with note
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
onCompleteWithPastDate?.()
handleMenuClose()
}}
>
<Update />
Complete in past
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleSkip()
}}
>
<SwitchAccessShortcut />
Skip to next due date
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
onChangeAssignee?.()
handleMenuClose()
}}
>
<RecordVoiceOver />
Delegate to someone else
</MenuItem>
{isOfficialInstance && (
<MenuItem
onClick={e => {
e.stopPropagation()
onNudge?.()
handleMenuClose()
}}
>
<Notifications />
Send nudge
</MenuItem>
)}
<Divider />
<MenuItem
onClick={e => {
e.stopPropagation()
handleHistory()
}}
>
<ManageSearch />
History
</MenuItem>
<Divider />
<MenuItem
sx={{
display: 'flex',
justifyContent: 'space-around',
alignItems: 'center',
gap: 1,
cursor: 'default',
'&:hover': {
backgroundColor: 'transparent',
},
}}
onClick={e => e.stopPropagation()}
>
<Tooltip title='Today' placement='top'>
<IconButton
size='sm'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('today')
}}
>
<Today />
</IconButton>
</Tooltip>
<Tooltip title='Tomorrow' placement='top'>
<IconButton
size='sm'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('tomorrow')
}}
>
<WbSunny />
</IconButton>
</Tooltip>
<Tooltip title='Weekend' placement='top'>
<IconButton
size='sm'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('weekend')
}}
>
<Weekend />
</IconButton>
</Tooltip>
<Tooltip title='Next week' placement='top'>
<IconButton
size='sm'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('next-week')
}}
>
<NextWeek />
</IconButton>
</Tooltip>
<Tooltip title='Remove due date' placement='top'>
<IconButton
size='sm'
color='neutral'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('remove')
}}
>
<Cancel />
</IconButton>
</Tooltip>
</MenuItem>
<Divider />
<MenuItem
onClick={e => {
e.stopPropagation()
onChangeDueDate?.()
handleMenuClose()
}}
>
<MoreTime />
Change due date
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
onWriteNFC?.()
handleMenuClose()
}}
>
<Nfc />
Write to NFC
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleEdit()
}}
>
<Edit />
Edit
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleClone()
}}
>
<CopyAll />
Clone
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleView()
}}
>
<ViewCarousel />
View
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleArchive()
}}
color='neutral'
>
{chore.isActive ? <Archive /> : <Unarchive />}
{chore.isActive ? 'Archive' : 'Unarchive'}
</MenuItem>
{projects.length > 0 && (
<MenuItem
onClick={e => {
e.stopPropagation()
setShowProjectPicker(true)
}}
>
<DriveFileMove />
Move to project
</MenuItem>
)}
<Divider />
<MenuItem
onClick={e => {
e.stopPropagation()
handleDelete()
}}
color='danger'
>
<Delete />
Delete
</MenuItem>
</>
)}
</Menu>
</>
)
}
export default ChoreActionMenu