feat: enhance chore management with new modals and filter options

This commit is contained in:
Mo Tarbin
2026-08-16 02:46:04 -04:00
parent 5359ae193b
commit 1c99bd1886
5 changed files with 520 additions and 163 deletions

View File

@@ -63,6 +63,8 @@
"skip": "Skip",
"cancel": "Cancel",
"noPriority": "No Priority",
"more": "More",
"changeDueDate": "Change due date",
"subtasks": "Subtasks",
"noDescription": "No description available",
"timer": {

View File

@@ -147,8 +147,19 @@ const FilterBar = ({
onClearAll,
resultCount,
totalCount,
// When the host renders its own trigger (e.g. an icon button in a toolbar
// row), it drives the sheet through `open`/`onOpenChange` and hides ours.
open,
onOpenChange,
showTrigger = true,
}) => {
const [isOpen, setIsOpen] = useState(false)
const [internalOpen, setInternalOpen] = useState(false)
const isControlled = open !== undefined
const isOpen = isControlled ? open : internalOpen
const setIsOpen = next => {
if (!isControlled) setInternalOpen(next)
onOpenChange?.(next)
}
// ── Active count ───────────────────────────────────────────────────────────
@@ -293,65 +304,75 @@ const FilterBar = ({
// ── Render ─────────────────────────────────────────────────────────────────
const activeChips = filterDefs
.map(def => ({ def, label: getActiveChipLabel(def) }))
.filter(({ label }) => !!label)
.map(({ def, label }) => ({
key: def.id,
label,
onClear: () => onSetFilter(def.id, null),
}))
// With the trigger hoisted into a toolbar, the inline row has nothing to show
// until a filter is on — rendering it anyway would leave a phantom gap.
const showInlineBar = showTrigger || activeChips.length > 0
return (
<>
{/* ── Inline bar ─────────────────────────────────────── */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap',
mb: 2,
}}
>
<Badge
badgeContent={activeFilterCount || null}
color='primary'
size='sm'
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
sx={{ display: 'flex', alignItems: 'center' }}
{showInlineBar && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap',
mb: 2,
}}
>
<Button
size='md'
variant={hasActive ? 'solid' : 'outlined'}
color={hasActive ? 'primary' : 'neutral'}
startDecorator={<FilterList sx={{ fontSize: 16 }} />}
onClick={() => setIsOpen(true)}
sx={{
borderRadius: 'xl',
py: 0.5,
px: 1,
gap: 0.5,
alignItems: 'center',
'& .MuiButton-startDecorator': {
display: 'flex',
alignItems: 'center',
mr: 0.5,
},
}}
>
Filters
</Button>
</Badge>
{showTrigger && (
<Badge
badgeContent={activeFilterCount || null}
color='primary'
size='sm'
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
sx={{ display: 'flex', alignItems: 'center' }}
>
<Button
size='md'
variant={hasActive ? 'solid' : 'outlined'}
color={hasActive ? 'primary' : 'neutral'}
startDecorator={<FilterList sx={{ fontSize: 16 }} />}
onClick={() => setIsOpen(true)}
sx={{
borderRadius: 'xl',
py: 0.5,
px: 1,
gap: 0.5,
alignItems: 'center',
'& .MuiButton-startDecorator': {
display: 'flex',
alignItems: 'center',
mr: 0.5,
},
}}
>
Filters
</Button>
</Badge>
)}
<ActiveFilterChips
chips={filterDefs
.map(def => ({ def, label: getActiveChipLabel(def) }))
.filter(({ label }) => !!label)
.map(({ def, label }) => ({
key: def.id,
label,
onClear: () => onSetFilter(def.id, null),
}))}
onOpen={() => setIsOpen(true)}
onClearAll={hasActive ? onClearAll : undefined}
resultCount={hasActive ? resultCount : undefined}
totalCount={hasActive ? totalCount : undefined}
maxVisible={2}
chipSize='md'
/>
</Box>
<ActiveFilterChips
chips={activeChips}
onOpen={() => setIsOpen(true)}
onClearAll={hasActive ? onClearAll : undefined}
resultCount={hasActive ? resultCount : undefined}
totalCount={hasActive ? totalCount : undefined}
maxVisible={2}
chipSize='md'
/>
</Box>
)}
{/* ── Bottom sheet ────────────────────────────────────── */}
<AppModal

View File

@@ -1,3 +1,4 @@
import { Capacitor } from '@capacitor/core'
import {
Archive,
AttachFile,
@@ -7,7 +8,7 @@ import {
Edit,
History,
HourglassEmpty,
LowPriority,
MoreVert,
OpenInFull,
PeopleAlt,
Person,
@@ -25,18 +26,13 @@ import {
Checkbox,
Chip,
Container,
Dropdown,
FormControl,
Grid,
IconButton,
Input,
Menu,
MenuButton,
MenuItem,
Sheet,
Typography,
} from '@mui/joy'
import { Divider } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import moment from 'moment'
import { useEffect, useState } from 'react'
@@ -68,20 +64,33 @@ import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import { commandQueue, CommandType } from '../../utils/CommandQueue'
import {
ApproveChore,
ArchiveChore,
DeleteChore,
GetChoreDetailById,
MarkChoreComplete,
NudgeChore,
RejectChore,
SaveChore,
SkipChore,
UnArchiveChore,
UndoChoreAction,
UpdateChoreAssignee,
UpdateChorePriority,
UpdateDueDate,
} from '../../utils/Fetcher'
import { offlineDB } from '../../utils/OfflineDB'
import Priorities from '../../utils/Priorities'
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
import AttachmentBrowserModal from '../Modals/Inputs/AttachmentBrowserModal'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
import NudgeModal from '../Modals/Inputs/NudgeModal'
import SelectModal from '../Modals/Inputs/SelectModal'
import WriteNFCModal from '../Modals/Inputs/WriteNFCModal'
import ChoreActionMenu from '../components/ChoreActionMenu'
import DueDatePickerModal, {
combineDueDate,
splitDueDate,
} from '../components/DueDatePickerModal'
import LoadingComponent from '../components/Loading.jsx'
import PendingBadge from '../components/PendingBadge'
import RichTextEditor from '../components/RichTextEditor.jsx'
@@ -106,6 +115,11 @@ const decodeHtmlEntities = value => {
const hasHtmlTags = value => /<\/?[a-z][\s\S]*>/i.test(value)
const getNFCUrl = choreId =>
Capacitor.getPlatform() === 'android' || Capacitor.getPlatform() === 'ios'
? `donetick://chores/${choreId}`
: `${window.location.origin}/chores/${choreId}`
const ChoreView = () => {
const { t } = useTranslation('chores')
const { fmt } = useLocalization()
@@ -124,7 +138,7 @@ const ChoreView = () => {
const [confirmModelConfig, setConfirmModelConfig] = useState({
isOpen: false,
})
const [chorePriority, setChorePriority] = useState(null)
const [activeModal, setActiveModal] = useState(null)
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
const [timerActionConfig, setTimerActionConfig] = useState({ isOpen: false })
const [attachmentBrowserOpen, setAttachmentBrowserOpen] = useState(false)
@@ -165,7 +179,6 @@ const ChoreView = () => {
return
}
setChore(choreData.res)
setChorePriority(Priorities.find(p => p.value === choreData.res.priority))
document.title = 'Donetick: ' + choreData.res.name
setPerformers(circleMembersData.res)
@@ -236,7 +249,7 @@ const ChoreView = () => {
UpdateChorePriority(choreId, priority.value).then(response => {
if (response.ok) {
response.json().then(() => {
setChorePriority(priority)
setChore(prev => ({ ...prev, priority: priority.value }))
queryClient.invalidateQueries(['chores'])
})
}
@@ -583,6 +596,168 @@ const ChoreView = () => {
}
}
const confirmSkipTask = () => {
setConfirmModelConfig({
isOpen: true,
title: t('choreView.skipTask'),
message: t('choreView.skipTaskConfirmation'),
confirmText: t('choreView.skip'),
cancelText: t('choreView.cancel'),
onClose: confirmed => {
if (confirmed) {
handleSkippingTask()
}
setConfirmModelConfig({})
},
})
}
const handleArchiveChore = async () => {
try {
const response = await ArchiveChore(choreId)
if (response.ok) {
await offlineDB.saveChores([{ ...chore, isActive: false }])
setChore({ ...chore, isActive: false })
queryClient.invalidateQueries(['chores'])
}
} catch (error) {
showError({
title: 'Failed to archive',
message: error?.message || 'Unable to archive task',
})
}
}
const confirmDeleteChore = () => {
setConfirmModelConfig({
isOpen: true,
title: 'Delete task',
message: 'Are you sure you want to delete this task?',
confirmText: 'Delete',
cancelText: t('choreView.cancel'),
onClose: async confirmed => {
setConfirmModelConfig({})
if (!confirmed) return
try {
const response = await DeleteChore(choreId)
if (response.ok) {
queryClient.invalidateQueries(['chores'])
showSuccess({
title: 'Task Deleted',
message: 'The task has been deleted successfully.',
})
navigate('/chores')
}
} catch (error) {
showError({
title: 'Failed to delete',
message: error?.message || 'Unable to delete task',
})
}
},
})
}
const handleDueDateChange = async newDate => {
try {
const response = await UpdateDueDate(choreId, newDate)
if (response.ok) {
setChore(prev => ({ ...prev, nextDueDate: newDate }))
queryClient.invalidateQueries(['chores'])
}
} catch (error) {
showError({
title: 'Failed to reschedule',
message: error?.message || 'Unable to change the due date',
})
}
}
const handleMoveToProject = async project => {
const projectId = project?.id ?? null
try {
const response = await SaveChore({ ...chore, projectId })
if (response.ok) {
setChore(prev => ({ ...prev, projectId }))
queryClient.invalidateQueries(['chores'])
showSuccess({
title: 'Task Moved',
message: `Task moved to ${project?.name || 'Default Project'}.`,
})
}
} catch (error) {
showError({
title: 'Failed to move task',
message: error?.message || 'Unable to move task to project',
})
}
}
const handleNudge = async ({ message, notifyAllAssignees }) => {
try {
const response = await NudgeChore(choreId, {
message,
notifyAllAssignees,
})
if (!response.ok) {
throw new Error('Failed to send nudge')
}
const data = await response.json()
showSuccess({
title: 'Nudge Sent!',
message: data.message || 'Nudge sent successfully',
})
} catch (error) {
showError({
title: 'Failed to Send Nudge',
message: error?.message || 'Unable to send nudge at this time',
})
}
}
const handleAssigneeChange = async assigneeId => {
try {
const response = await UpdateChoreAssignee(choreId, assigneeId)
if (response.ok) {
const data = await response.json()
setChore(data.res)
queryClient.invalidateQueries(['chores'])
}
} catch (error) {
showError({
title: 'Failed to delegate',
message: error?.message || 'Unable to change the assignee',
})
}
}
// Actions the menu raises that ChoreView owns; the rest of its items either
// navigate on their own or come in through the dedicated callbacks.
const handleMenuAction = (type, _chore, extraData) => {
switch (type) {
case 'skip':
confirmSkipTask()
break
case 'archive':
handleArchiveChore()
break
case 'unarchive':
handleUnarchiveChore()
break
case 'delete':
confirmDeleteChore()
break
case 'changeDueDate':
handleDueDateChange(extraData?.date?.toISOString() ?? null)
break
case 'moveToProject':
handleMoveToProject(extraData?.project)
break
default:
break
}
}
// Check if the current user can approve/reject (admin, manager, or task owner)
const canApproveReject = () => {
if (!circleMembersData?.res || !chore) return false
@@ -794,64 +969,6 @@ const ChoreView = () => {
mb: 1,
}}
>
<Dropdown>
<MenuButton
disabled={chore.isActive === false}
color={
chorePriority?.name === 'P1'
? 'danger'
: chorePriority?.name === 'P2'
? 'warning'
: 'neutral'
}
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
p: 1,
width: '100%',
}}
variant='plain'
>
{chorePriority ? chorePriority.icon : <LowPriority />}
{chorePriority ? chorePriority.name : t('choreView.noPriority')}
</MenuButton>
<Menu>
{Priorities.map((priority, index) => (
<MenuItem
sx={{
pr: 1,
py: 1,
}}
key={index}
onClick={() => {
handleUpdatePriority(priority)
}}
color={priority.color}
>
{priority.icon}
{priority.name}
</MenuItem>
))}
<Divider />
<MenuItem
sx={{
pr: 1,
py: 1,
}}
onClick={() => {
handleUpdatePriority({
name: t('choreView.noPriority'),
value: 0,
})
setChorePriority(null)
}}
>
{t('choreView.noPriority')}
</MenuItem>
</Menu>
</Dropdown>
<Button
size='sm'
color='neutral'
@@ -889,6 +1006,38 @@ const ChoreView = () => {
<Edit />
Edit
</Button>
<ChoreActionMenu
chore={chore}
hiddenActions={['view']}
onAction={handleMenuAction}
onNudge={() => setActiveModal('nudge')}
onWriteNFC={() => setActiveModal('writeNFC')}
onCompleteWithNote={() => setNote('')}
onCompleteWithPastDate={() =>
setCompletedDate(moment(new Date()).format('YYYY-MM-DDTHH:00:00'))
}
onChangeAssignee={() => setActiveModal('changeAssignee')}
onChangeDueDate={() => setActiveModal('changeDueDate')}
onChangePriority={handleUpdatePriority}
onDelete={confirmDeleteChore}
trigger={
<Button
size='sm'
color='neutral'
variant='plain'
fullWidth
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
p: 1,
}}
>
<MoreVert />
{t('choreView.more', 'More')}
</Button>
}
/>
</Box>
{chore.description && (
@@ -1261,21 +1410,7 @@ const ChoreView = () => {
<Button
fullWidth
size='lg'
onClick={() => {
setConfirmModelConfig({
isOpen: true,
title: t('choreView.skipTask'),
message: t('choreView.skipTaskConfirmation'),
confirmText: t('choreView.skip'),
cancelText: t('choreView.cancel'),
onClose: confirmed => {
if (confirmed) {
handleSkippingTask()
}
setConfirmModelConfig({})
},
})
}}
onClick={confirmSkipTask}
disabled={
notInCompletionWindow(chore) || chore.isActive === false
}
@@ -1348,6 +1483,52 @@ const ChoreView = () => {
<ConfirmationModal config={confirmModelConfig} />
<ConfirmationModal config={timerActionConfig} />
<NoteViewerModal config={noteViewerConfig} />
{activeModal === 'changeDueDate' && (
<DueDatePickerModal
open={true}
title={t('choreView.changeDueDate', 'Change due date')}
{...splitDueDate(chore.nextDueDate)}
onClose={() => setActiveModal(null)}
onApply={parts => {
handleDueDateChange(combineDueDate(parts)?.toISOString() ?? null)
setActiveModal(null)
}}
onRemove={() => {
handleDueDateChange(null)
setActiveModal(null)
}}
/>
)}
{activeModal === 'changeAssignee' && (
<SelectModal
isOpen={true}
options={performers}
displayKey='displayName'
title='Delegate to someone else'
placeholder='Select a performer'
onClose={() => setActiveModal(null)}
onSave={selected => handleAssigneeChange(selected.id)}
/>
)}
{activeModal === 'nudge' && (
<NudgeModal
config={{
isOpen: true,
choreId: chore.id,
onClose: () => setActiveModal(null),
onConfirm: handleNudge,
}}
/>
)}
{activeModal === 'writeNFC' && (
<WriteNFCModal
config={{
isOpen: true,
url: getNFCUrl(choreId),
onClose: () => setActiveModal(null),
}}
/>
)}
<AttachmentBrowserModal
choreId={choreId}
isOpen={attachmentBrowserOpen}

View File

@@ -4,6 +4,7 @@ import {
CheckBoxOutlineBlank,
Close,
Delete,
FilterList,
Label,
Person,
PriorityHigh,
@@ -14,6 +15,7 @@ import {
ViewModule,
} from '@mui/icons-material'
import {
Badge,
Box,
Button,
Container,
@@ -202,6 +204,7 @@ const ArchivedTasks = () => {
)
const {
activeFilterCount,
activeFilters,
clearAll,
filteredData: filteredByBar,
@@ -209,6 +212,8 @@ const ArchivedTasks = () => {
setFilter,
} = useFilter(filteredChores, filterDefs)
const [isFilterSheetOpen, setIsFilterSheetOpen] = useState(false)
const [sortBy, setSortBy] = useState(
() => localStorage.getItem('archivedChoresSortBy') || 'archivedAt',
)
@@ -770,6 +775,30 @@ const ArchivedTasks = () => {
}
/>
{/* Filter Sheet Trigger */}
<Badge
badgeContent={activeFilterCount || null}
color='primary'
size='sm'
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
sx={{ display: 'flex', alignItems: 'center' }}
>
<IconButton
variant={hasActiveFilters ? 'solid' : 'outlined'}
color={hasActiveFilters ? 'primary' : 'neutral'}
size='sm'
sx={{
height: 32,
width: 32,
borderRadius: '50%',
}}
onClick={() => setIsFilterSheetOpen(true)}
title='Filters'
>
<FilterList />
</IconButton>
</Badge>
{/* Sort Menu */}
<SortAndFilterMenu
sortOptions={[
@@ -845,6 +874,9 @@ const ArchivedTasks = () => {
onClearAll={clearAll}
resultCount={finalChores.length}
totalCount={filteredChores.length}
showTrigger={false}
open={isFilterSheetOpen}
onOpenChange={setIsFilterSheetOpen}
/>
{/* Multi-select Toolbar */}

View File

@@ -6,6 +6,7 @@ import {
Delete,
DriveFileMove,
Edit,
Flag,
ManageSearch,
MoreTime,
MoreVert,
@@ -25,6 +26,7 @@ import {
import {
Avatar,
Button,
Chip,
Divider,
IconButton,
List,
@@ -45,9 +47,21 @@ import LABEL_COLORS, {
getTextColorFromBackgroundColor,
} from '../../utils/Colors'
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
import Priorities from '../../utils/Priorities'
import { getIconComponent } from '../../utils/ProjectIcons'
import { useProjects } from '../Projects/ProjectQueries'
const NO_PRIORITY = { name: 'No priority', value: 0, color: 'neutral' }
// After hiding actions the caller does not support, the dividers around them
// would otherwise stack up or dangle at the edges of the list.
const collapseDividers = items =>
items.filter((item, index) => {
if (item.type !== 'divider') return true
if (index === 0 || index === items.length - 1) return false
return items[index - 1].type !== 'divider'
})
const ChoreActionMenu = ({
chore,
onAction,
@@ -55,18 +69,22 @@ const ChoreActionMenu = ({
onCompleteWithPastDate,
onChangeAssignee,
onChangeDueDate,
onChangePriority,
onWriteNFC,
onNudge,
onDelete,
onOpen,
onMouseEnter,
onMouseLeave,
hiddenActions = [],
trigger,
sx = {},
variant = 'soft',
}) => {
const [anchorEl, setAnchorEl] = React.useState(null)
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
const [showProjectPicker, setShowProjectPicker] = useState(false)
const [showPriorityPicker, setShowPriorityPicker] = useState(false)
const menuRef = React.useRef(null)
const navigate = useNavigate()
const { data: projects = [] } = useProjects()
@@ -118,6 +136,12 @@ const ChoreActionMenu = ({
const handleMenuClose = () => {
setAnchorEl(null)
setShowProjectPicker(false)
setShowPriorityPicker(false)
}
const handleChangePriority = priority => {
onChangePriority?.(priority)
handleMenuClose()
}
const handleMoveToProject = project => {
@@ -245,6 +269,9 @@ const ChoreActionMenu = ({
)
}
const currentPriority =
Priorities.find(p => p.value === chore?.priority) || null
// Shared action list, rendered as MenuItems on large screens and as a
// ListItemButton list inside an AppModal sheet on small screens.
const actionItems = [
@@ -309,6 +336,21 @@ const ChoreActionMenu = ({
handleMenuClose()
},
},
onChangePriority && {
key: 'priority',
icon: <Flag />,
label: 'Priority',
onClick: () => setShowPriorityPicker(true),
endDecorator: (
<Chip
size='sm'
variant='soft'
color={currentPriority?.color || 'neutral'}
>
{currentPriority?.name.trim() || 'None'}
</Chip>
),
},
{
key: 'writeNfc',
icon: <Nfc />,
@@ -342,7 +384,11 @@ const ChoreActionMenu = ({
onClick: handleDelete,
color: 'danger',
},
].filter(Boolean)
]
.filter(Boolean)
.filter(item => !hiddenActions.includes(item.key))
const visibleActionItems = collapseDividers(actionItems)
const quickScheduleButtons = (
<>
@@ -415,7 +461,7 @@ const ChoreActionMenu = ({
}
const renderMenuActionItems = () =>
actionItems.map(item => {
visibleActionItems.map(item => {
if (item.type === 'divider') return <Divider key={item.key} />
if (item.type === 'quickSchedule') {
return (
@@ -443,12 +489,17 @@ const ChoreActionMenu = ({
>
{item.icon}
{item.label}
{item.endDecorator && (
<ListItemDecorator sx={{ ml: 'auto', minInlineSize: 0 }}>
{item.endDecorator}
</ListItemDecorator>
)}
</MenuItem>
)
})
const renderModalActionItems = () =>
actionItems.map(item => {
visibleActionItems.map(item => {
if (item.type === 'divider') return <Divider key={item.key} />
if (item.type === 'quickSchedule') {
return (
@@ -462,11 +513,60 @@ const ChoreActionMenu = ({
<ListItemButton color={item.color} onClick={() => item.onClick()}>
<ListItemDecorator>{item.icon}</ListItemDecorator>
<ListItemContent>{item.label}</ListItemContent>
{item.endDecorator}
</ListItemButton>
</ListItem>
)
})
const priorityOptions = [...Priorities, NO_PRIORITY]
const renderModalPriorityPicker = () =>
priorityOptions.map(priority => (
<ListItem key={priority.value}>
<ListItemButton
selected={(currentPriority?.value || 0) === priority.value}
color={priority.color || 'neutral'}
onClick={() => handleChangePriority(priority)}
>
<ListItemDecorator>{priority.icon || <Flag />}</ListItemDecorator>
<ListItemContent>{priority.name.trim()}</ListItemContent>
</ListItemButton>
</ListItem>
))
const renderMenuPriorityPicker = () => (
<>
<MenuItem
onClick={e => {
e.stopPropagation()
setShowPriorityPicker(false)
}}
sx={{ gap: 1 }}
>
<ArrowBack fontSize='small' />
<Typography level='body-sm' fontWeight={600}>
Priority
</Typography>
</MenuItem>
<Divider />
{priorityOptions.map(priority => (
<MenuItem
key={priority.value}
selected={(currentPriority?.value || 0) === priority.value}
color={priority.color || 'neutral'}
onClick={e => {
e.stopPropagation()
handleChangePriority(priority)
}}
>
{priority.icon || <Flag />}
{priority.name.trim()}
</MenuItem>
))}
</>
)
const renderModalProjectPicker = () => (
<>
<ListItem>
@@ -496,40 +596,57 @@ const ChoreActionMenu = ({
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>
{trigger ? (
React.cloneElement(trigger, {
onClick: handleMenuOpen,
onMouseEnter,
onMouseLeave,
})
) : (
<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>
)}
{isSmallScreen ? (
<AppModal
open={Boolean(anchorEl)}
onClose={handleMenuClose}
title={showProjectPicker ? 'Move to project' : chore?.name}
title={
showProjectPicker
? 'Move to project'
: showPriorityPicker
? 'Priority'
: chore?.name
}
mobilePresentation='sheet'
showHandle
contentSx={{ px: 0, pb: 1 }}
>
{showProjectPicker && (
{(showProjectPicker || showPriorityPicker) && (
<Button
variant='plain'
color='neutral'
size='sm'
startDecorator={<ArrowBack fontSize='small' />}
onClick={() => setShowProjectPicker(false)}
onClick={() => {
setShowProjectPicker(false)
setShowPriorityPicker(false)
}}
sx={{ mx: 2, mb: 1 }}
>
<Typography level='body-sm' fontWeight={600}>
@@ -540,7 +657,9 @@ const ChoreActionMenu = ({
<List sx={{ '--ListItem-radius': '8px', px: 1 }}>
{showProjectPicker
? renderModalProjectPicker()
: renderModalActionItems()}
: showPriorityPicker
? renderModalPriorityPicker()
: renderModalActionItems()}
</List>
</AppModal>
) : (
@@ -556,7 +675,9 @@ const ChoreActionMenu = ({
left: '50%',
}}
>
{showProjectPicker ? (
{showPriorityPicker ? (
renderMenuPriorityPicker()
) : showProjectPicker ? (
<>
<MenuItem
onClick={e => {