feat: enhance chore management with new modals and filter options
This commit is contained in:
@@ -63,6 +63,8 @@
|
|||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"noPriority": "No Priority",
|
"noPriority": "No Priority",
|
||||||
|
"more": "More",
|
||||||
|
"changeDueDate": "Change due date",
|
||||||
"subtasks": "Subtasks",
|
"subtasks": "Subtasks",
|
||||||
"noDescription": "No description available",
|
"noDescription": "No description available",
|
||||||
"timer": {
|
"timer": {
|
||||||
|
|||||||
@@ -147,8 +147,19 @@ const FilterBar = ({
|
|||||||
onClearAll,
|
onClearAll,
|
||||||
resultCount,
|
resultCount,
|
||||||
totalCount,
|
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 ───────────────────────────────────────────────────────────
|
// ── Active count ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -293,65 +304,75 @@ const FilterBar = ({
|
|||||||
|
|
||||||
// ── Render ─────────────────────────────────────────────────────────────────
|
// ── 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ── Inline bar ─────────────────────────────────────── */}
|
{/* ── Inline bar ─────────────────────────────────────── */}
|
||||||
<Box
|
{showInlineBar && (
|
||||||
sx={{
|
<Box
|
||||||
display: 'flex',
|
sx={{
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
gap: 1,
|
alignItems: 'center',
|
||||||
flexWrap: 'wrap',
|
gap: 1,
|
||||||
mb: 2,
|
flexWrap: 'wrap',
|
||||||
}}
|
mb: 2,
|
||||||
>
|
}}
|
||||||
<Badge
|
|
||||||
badgeContent={activeFilterCount || null}
|
|
||||||
color='primary'
|
|
||||||
size='sm'
|
|
||||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
|
||||||
sx={{ display: 'flex', alignItems: 'center' }}
|
|
||||||
>
|
>
|
||||||
<Button
|
{showTrigger && (
|
||||||
size='md'
|
<Badge
|
||||||
variant={hasActive ? 'solid' : 'outlined'}
|
badgeContent={activeFilterCount || null}
|
||||||
color={hasActive ? 'primary' : 'neutral'}
|
color='primary'
|
||||||
startDecorator={<FilterList sx={{ fontSize: 16 }} />}
|
size='sm'
|
||||||
onClick={() => setIsOpen(true)}
|
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
sx={{
|
sx={{ display: 'flex', alignItems: 'center' }}
|
||||||
borderRadius: 'xl',
|
>
|
||||||
py: 0.5,
|
<Button
|
||||||
px: 1,
|
size='md'
|
||||||
gap: 0.5,
|
variant={hasActive ? 'solid' : 'outlined'}
|
||||||
alignItems: 'center',
|
color={hasActive ? 'primary' : 'neutral'}
|
||||||
'& .MuiButton-startDecorator': {
|
startDecorator={<FilterList sx={{ fontSize: 16 }} />}
|
||||||
display: 'flex',
|
onClick={() => setIsOpen(true)}
|
||||||
alignItems: 'center',
|
sx={{
|
||||||
mr: 0.5,
|
borderRadius: 'xl',
|
||||||
},
|
py: 0.5,
|
||||||
}}
|
px: 1,
|
||||||
>
|
gap: 0.5,
|
||||||
Filters
|
alignItems: 'center',
|
||||||
</Button>
|
'& .MuiButton-startDecorator': {
|
||||||
</Badge>
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
mr: 0.5,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Filters
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
<ActiveFilterChips
|
<ActiveFilterChips
|
||||||
chips={filterDefs
|
chips={activeChips}
|
||||||
.map(def => ({ def, label: getActiveChipLabel(def) }))
|
onOpen={() => setIsOpen(true)}
|
||||||
.filter(({ label }) => !!label)
|
onClearAll={hasActive ? onClearAll : undefined}
|
||||||
.map(({ def, label }) => ({
|
resultCount={hasActive ? resultCount : undefined}
|
||||||
key: def.id,
|
totalCount={hasActive ? totalCount : undefined}
|
||||||
label,
|
maxVisible={2}
|
||||||
onClear: () => onSetFilter(def.id, null),
|
chipSize='md'
|
||||||
}))}
|
/>
|
||||||
onOpen={() => setIsOpen(true)}
|
</Box>
|
||||||
onClearAll={hasActive ? onClearAll : undefined}
|
)}
|
||||||
resultCount={hasActive ? resultCount : undefined}
|
|
||||||
totalCount={hasActive ? totalCount : undefined}
|
|
||||||
maxVisible={2}
|
|
||||||
chipSize='md'
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* ── Bottom sheet ────────────────────────────────────── */}
|
{/* ── Bottom sheet ────────────────────────────────────── */}
|
||||||
<AppModal
|
<AppModal
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
import {
|
import {
|
||||||
Archive,
|
Archive,
|
||||||
AttachFile,
|
AttachFile,
|
||||||
@@ -7,7 +8,7 @@ import {
|
|||||||
Edit,
|
Edit,
|
||||||
History,
|
History,
|
||||||
HourglassEmpty,
|
HourglassEmpty,
|
||||||
LowPriority,
|
MoreVert,
|
||||||
OpenInFull,
|
OpenInFull,
|
||||||
PeopleAlt,
|
PeopleAlt,
|
||||||
Person,
|
Person,
|
||||||
@@ -25,18 +26,13 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
Chip,
|
Chip,
|
||||||
Container,
|
Container,
|
||||||
Dropdown,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
Grid,
|
Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuItem,
|
|
||||||
Sheet,
|
Sheet,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { Divider } from '@mui/material'
|
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -68,20 +64,33 @@ import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
|||||||
import { commandQueue, CommandType } from '../../utils/CommandQueue'
|
import { commandQueue, CommandType } from '../../utils/CommandQueue'
|
||||||
import {
|
import {
|
||||||
ApproveChore,
|
ApproveChore,
|
||||||
|
ArchiveChore,
|
||||||
|
DeleteChore,
|
||||||
GetChoreDetailById,
|
GetChoreDetailById,
|
||||||
MarkChoreComplete,
|
MarkChoreComplete,
|
||||||
|
NudgeChore,
|
||||||
RejectChore,
|
RejectChore,
|
||||||
|
SaveChore,
|
||||||
SkipChore,
|
SkipChore,
|
||||||
UnArchiveChore,
|
UnArchiveChore,
|
||||||
UndoChoreAction,
|
UndoChoreAction,
|
||||||
|
UpdateChoreAssignee,
|
||||||
UpdateChorePriority,
|
UpdateChorePriority,
|
||||||
|
UpdateDueDate,
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
import { offlineDB } from '../../utils/OfflineDB'
|
import { offlineDB } from '../../utils/OfflineDB'
|
||||||
import Priorities from '../../utils/Priorities'
|
|
||||||
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
|
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
|
||||||
import AttachmentBrowserModal from '../Modals/Inputs/AttachmentBrowserModal'
|
import AttachmentBrowserModal from '../Modals/Inputs/AttachmentBrowserModal'
|
||||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
|
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 LoadingComponent from '../components/Loading.jsx'
|
||||||
import PendingBadge from '../components/PendingBadge'
|
import PendingBadge from '../components/PendingBadge'
|
||||||
import RichTextEditor from '../components/RichTextEditor.jsx'
|
import RichTextEditor from '../components/RichTextEditor.jsx'
|
||||||
@@ -106,6 +115,11 @@ const decodeHtmlEntities = value => {
|
|||||||
|
|
||||||
const hasHtmlTags = value => /<\/?[a-z][\s\S]*>/i.test(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 ChoreView = () => {
|
||||||
const { t } = useTranslation('chores')
|
const { t } = useTranslation('chores')
|
||||||
const { fmt } = useLocalization()
|
const { fmt } = useLocalization()
|
||||||
@@ -124,7 +138,7 @@ const ChoreView = () => {
|
|||||||
const [confirmModelConfig, setConfirmModelConfig] = useState({
|
const [confirmModelConfig, setConfirmModelConfig] = useState({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
})
|
})
|
||||||
const [chorePriority, setChorePriority] = useState(null)
|
const [activeModal, setActiveModal] = useState(null)
|
||||||
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
|
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
|
||||||
const [timerActionConfig, setTimerActionConfig] = useState({ isOpen: false })
|
const [timerActionConfig, setTimerActionConfig] = useState({ isOpen: false })
|
||||||
const [attachmentBrowserOpen, setAttachmentBrowserOpen] = useState(false)
|
const [attachmentBrowserOpen, setAttachmentBrowserOpen] = useState(false)
|
||||||
@@ -165,7 +179,6 @@ const ChoreView = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setChore(choreData.res)
|
setChore(choreData.res)
|
||||||
setChorePriority(Priorities.find(p => p.value === choreData.res.priority))
|
|
||||||
document.title = 'Donetick: ' + choreData.res.name
|
document.title = 'Donetick: ' + choreData.res.name
|
||||||
|
|
||||||
setPerformers(circleMembersData.res)
|
setPerformers(circleMembersData.res)
|
||||||
@@ -236,7 +249,7 @@ const ChoreView = () => {
|
|||||||
UpdateChorePriority(choreId, priority.value).then(response => {
|
UpdateChorePriority(choreId, priority.value).then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
response.json().then(() => {
|
response.json().then(() => {
|
||||||
setChorePriority(priority)
|
setChore(prev => ({ ...prev, priority: priority.value }))
|
||||||
queryClient.invalidateQueries(['chores'])
|
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)
|
// Check if the current user can approve/reject (admin, manager, or task owner)
|
||||||
const canApproveReject = () => {
|
const canApproveReject = () => {
|
||||||
if (!circleMembersData?.res || !chore) return false
|
if (!circleMembersData?.res || !chore) return false
|
||||||
@@ -794,64 +969,6 @@ const ChoreView = () => {
|
|||||||
mb: 1,
|
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
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
@@ -889,6 +1006,38 @@ const ChoreView = () => {
|
|||||||
<Edit />
|
<Edit />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</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>
|
</Box>
|
||||||
|
|
||||||
{chore.description && (
|
{chore.description && (
|
||||||
@@ -1261,21 +1410,7 @@ const ChoreView = () => {
|
|||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
size='lg'
|
size='lg'
|
||||||
onClick={() => {
|
onClick={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({})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
disabled={
|
disabled={
|
||||||
notInCompletionWindow(chore) || chore.isActive === false
|
notInCompletionWindow(chore) || chore.isActive === false
|
||||||
}
|
}
|
||||||
@@ -1348,6 +1483,52 @@ const ChoreView = () => {
|
|||||||
<ConfirmationModal config={confirmModelConfig} />
|
<ConfirmationModal config={confirmModelConfig} />
|
||||||
<ConfirmationModal config={timerActionConfig} />
|
<ConfirmationModal config={timerActionConfig} />
|
||||||
<NoteViewerModal config={noteViewerConfig} />
|
<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
|
<AttachmentBrowserModal
|
||||||
choreId={choreId}
|
choreId={choreId}
|
||||||
isOpen={attachmentBrowserOpen}
|
isOpen={attachmentBrowserOpen}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
CheckBoxOutlineBlank,
|
CheckBoxOutlineBlank,
|
||||||
Close,
|
Close,
|
||||||
Delete,
|
Delete,
|
||||||
|
FilterList,
|
||||||
Label,
|
Label,
|
||||||
Person,
|
Person,
|
||||||
PriorityHigh,
|
PriorityHigh,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
ViewModule,
|
ViewModule,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Container,
|
Container,
|
||||||
@@ -202,6 +204,7 @@ const ArchivedTasks = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
activeFilterCount,
|
||||||
activeFilters,
|
activeFilters,
|
||||||
clearAll,
|
clearAll,
|
||||||
filteredData: filteredByBar,
|
filteredData: filteredByBar,
|
||||||
@@ -209,6 +212,8 @@ const ArchivedTasks = () => {
|
|||||||
setFilter,
|
setFilter,
|
||||||
} = useFilter(filteredChores, filterDefs)
|
} = useFilter(filteredChores, filterDefs)
|
||||||
|
|
||||||
|
const [isFilterSheetOpen, setIsFilterSheetOpen] = useState(false)
|
||||||
|
|
||||||
const [sortBy, setSortBy] = useState(
|
const [sortBy, setSortBy] = useState(
|
||||||
() => localStorage.getItem('archivedChoresSortBy') || 'archivedAt',
|
() => 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 */}
|
{/* Sort Menu */}
|
||||||
<SortAndFilterMenu
|
<SortAndFilterMenu
|
||||||
sortOptions={[
|
sortOptions={[
|
||||||
@@ -845,6 +874,9 @@ const ArchivedTasks = () => {
|
|||||||
onClearAll={clearAll}
|
onClearAll={clearAll}
|
||||||
resultCount={finalChores.length}
|
resultCount={finalChores.length}
|
||||||
totalCount={filteredChores.length}
|
totalCount={filteredChores.length}
|
||||||
|
showTrigger={false}
|
||||||
|
open={isFilterSheetOpen}
|
||||||
|
onOpenChange={setIsFilterSheetOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Multi-select Toolbar */}
|
{/* Multi-select Toolbar */}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
DriveFileMove,
|
DriveFileMove,
|
||||||
Edit,
|
Edit,
|
||||||
|
Flag,
|
||||||
ManageSearch,
|
ManageSearch,
|
||||||
MoreTime,
|
MoreTime,
|
||||||
MoreVert,
|
MoreVert,
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
|
Chip,
|
||||||
Divider,
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
@@ -45,9 +47,21 @@ import LABEL_COLORS, {
|
|||||||
getTextColorFromBackgroundColor,
|
getTextColorFromBackgroundColor,
|
||||||
} from '../../utils/Colors'
|
} from '../../utils/Colors'
|
||||||
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
|
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
|
||||||
|
import Priorities from '../../utils/Priorities'
|
||||||
import { getIconComponent } from '../../utils/ProjectIcons'
|
import { getIconComponent } from '../../utils/ProjectIcons'
|
||||||
import { useProjects } from '../Projects/ProjectQueries'
|
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 = ({
|
const ChoreActionMenu = ({
|
||||||
chore,
|
chore,
|
||||||
onAction,
|
onAction,
|
||||||
@@ -55,18 +69,22 @@ const ChoreActionMenu = ({
|
|||||||
onCompleteWithPastDate,
|
onCompleteWithPastDate,
|
||||||
onChangeAssignee,
|
onChangeAssignee,
|
||||||
onChangeDueDate,
|
onChangeDueDate,
|
||||||
|
onChangePriority,
|
||||||
onWriteNFC,
|
onWriteNFC,
|
||||||
onNudge,
|
onNudge,
|
||||||
onDelete,
|
onDelete,
|
||||||
onOpen,
|
onOpen,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
|
hiddenActions = [],
|
||||||
|
trigger,
|
||||||
sx = {},
|
sx = {},
|
||||||
variant = 'soft',
|
variant = 'soft',
|
||||||
}) => {
|
}) => {
|
||||||
const [anchorEl, setAnchorEl] = React.useState(null)
|
const [anchorEl, setAnchorEl] = React.useState(null)
|
||||||
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
|
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
|
||||||
const [showProjectPicker, setShowProjectPicker] = useState(false)
|
const [showProjectPicker, setShowProjectPicker] = useState(false)
|
||||||
|
const [showPriorityPicker, setShowPriorityPicker] = useState(false)
|
||||||
const menuRef = React.useRef(null)
|
const menuRef = React.useRef(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { data: projects = [] } = useProjects()
|
const { data: projects = [] } = useProjects()
|
||||||
@@ -118,6 +136,12 @@ const ChoreActionMenu = ({
|
|||||||
const handleMenuClose = () => {
|
const handleMenuClose = () => {
|
||||||
setAnchorEl(null)
|
setAnchorEl(null)
|
||||||
setShowProjectPicker(false)
|
setShowProjectPicker(false)
|
||||||
|
setShowPriorityPicker(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangePriority = priority => {
|
||||||
|
onChangePriority?.(priority)
|
||||||
|
handleMenuClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMoveToProject = project => {
|
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
|
// Shared action list, rendered as MenuItems on large screens and as a
|
||||||
// ListItemButton list inside an AppModal sheet on small screens.
|
// ListItemButton list inside an AppModal sheet on small screens.
|
||||||
const actionItems = [
|
const actionItems = [
|
||||||
@@ -309,6 +336,21 @@ const ChoreActionMenu = ({
|
|||||||
handleMenuClose()
|
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',
|
key: 'writeNfc',
|
||||||
icon: <Nfc />,
|
icon: <Nfc />,
|
||||||
@@ -342,7 +384,11 @@ const ChoreActionMenu = ({
|
|||||||
onClick: handleDelete,
|
onClick: handleDelete,
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
].filter(Boolean)
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(item => !hiddenActions.includes(item.key))
|
||||||
|
|
||||||
|
const visibleActionItems = collapseDividers(actionItems)
|
||||||
|
|
||||||
const quickScheduleButtons = (
|
const quickScheduleButtons = (
|
||||||
<>
|
<>
|
||||||
@@ -415,7 +461,7 @@ const ChoreActionMenu = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderMenuActionItems = () =>
|
const renderMenuActionItems = () =>
|
||||||
actionItems.map(item => {
|
visibleActionItems.map(item => {
|
||||||
if (item.type === 'divider') return <Divider key={item.key} />
|
if (item.type === 'divider') return <Divider key={item.key} />
|
||||||
if (item.type === 'quickSchedule') {
|
if (item.type === 'quickSchedule') {
|
||||||
return (
|
return (
|
||||||
@@ -443,12 +489,17 @@ const ChoreActionMenu = ({
|
|||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
{item.label}
|
{item.label}
|
||||||
|
{item.endDecorator && (
|
||||||
|
<ListItemDecorator sx={{ ml: 'auto', minInlineSize: 0 }}>
|
||||||
|
{item.endDecorator}
|
||||||
|
</ListItemDecorator>
|
||||||
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderModalActionItems = () =>
|
const renderModalActionItems = () =>
|
||||||
actionItems.map(item => {
|
visibleActionItems.map(item => {
|
||||||
if (item.type === 'divider') return <Divider key={item.key} />
|
if (item.type === 'divider') return <Divider key={item.key} />
|
||||||
if (item.type === 'quickSchedule') {
|
if (item.type === 'quickSchedule') {
|
||||||
return (
|
return (
|
||||||
@@ -462,11 +513,60 @@ const ChoreActionMenu = ({
|
|||||||
<ListItemButton color={item.color} onClick={() => item.onClick()}>
|
<ListItemButton color={item.color} onClick={() => item.onClick()}>
|
||||||
<ListItemDecorator>{item.icon}</ListItemDecorator>
|
<ListItemDecorator>{item.icon}</ListItemDecorator>
|
||||||
<ListItemContent>{item.label}</ListItemContent>
|
<ListItemContent>{item.label}</ListItemContent>
|
||||||
|
{item.endDecorator}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</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 = () => (
|
const renderModalProjectPicker = () => (
|
||||||
<>
|
<>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -496,40 +596,57 @@ const ChoreActionMenu = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
{trigger ? (
|
||||||
variant={variant}
|
React.cloneElement(trigger, {
|
||||||
color='success'
|
onClick: handleMenuOpen,
|
||||||
onClick={handleMenuOpen}
|
onMouseEnter,
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseLeave,
|
||||||
onMouseLeave={onMouseLeave}
|
})
|
||||||
sx={{
|
) : (
|
||||||
borderRadius: '50%',
|
<IconButton
|
||||||
width: 25,
|
variant={variant}
|
||||||
height: 25,
|
color='success'
|
||||||
position: 'relative',
|
onClick={handleMenuOpen}
|
||||||
left: -10,
|
onMouseEnter={onMouseEnter}
|
||||||
...sx,
|
onMouseLeave={onMouseLeave}
|
||||||
}}
|
sx={{
|
||||||
>
|
borderRadius: '50%',
|
||||||
<MoreVert />
|
width: 25,
|
||||||
</IconButton>
|
height: 25,
|
||||||
|
position: 'relative',
|
||||||
|
left: -10,
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreVert />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
{isSmallScreen ? (
|
{isSmallScreen ? (
|
||||||
<AppModal
|
<AppModal
|
||||||
open={Boolean(anchorEl)}
|
open={Boolean(anchorEl)}
|
||||||
onClose={handleMenuClose}
|
onClose={handleMenuClose}
|
||||||
title={showProjectPicker ? 'Move to project' : chore?.name}
|
title={
|
||||||
|
showProjectPicker
|
||||||
|
? 'Move to project'
|
||||||
|
: showPriorityPicker
|
||||||
|
? 'Priority'
|
||||||
|
: chore?.name
|
||||||
|
}
|
||||||
mobilePresentation='sheet'
|
mobilePresentation='sheet'
|
||||||
showHandle
|
showHandle
|
||||||
contentSx={{ px: 0, pb: 1 }}
|
contentSx={{ px: 0, pb: 1 }}
|
||||||
>
|
>
|
||||||
{showProjectPicker && (
|
{(showProjectPicker || showPriorityPicker) && (
|
||||||
<Button
|
<Button
|
||||||
variant='plain'
|
variant='plain'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
size='sm'
|
size='sm'
|
||||||
startDecorator={<ArrowBack fontSize='small' />}
|
startDecorator={<ArrowBack fontSize='small' />}
|
||||||
onClick={() => setShowProjectPicker(false)}
|
onClick={() => {
|
||||||
|
setShowProjectPicker(false)
|
||||||
|
setShowPriorityPicker(false)
|
||||||
|
}}
|
||||||
sx={{ mx: 2, mb: 1 }}
|
sx={{ mx: 2, mb: 1 }}
|
||||||
>
|
>
|
||||||
<Typography level='body-sm' fontWeight={600}>
|
<Typography level='body-sm' fontWeight={600}>
|
||||||
@@ -540,7 +657,9 @@ const ChoreActionMenu = ({
|
|||||||
<List sx={{ '--ListItem-radius': '8px', px: 1 }}>
|
<List sx={{ '--ListItem-radius': '8px', px: 1 }}>
|
||||||
{showProjectPicker
|
{showProjectPicker
|
||||||
? renderModalProjectPicker()
|
? renderModalProjectPicker()
|
||||||
: renderModalActionItems()}
|
: showPriorityPicker
|
||||||
|
? renderModalPriorityPicker()
|
||||||
|
: renderModalActionItems()}
|
||||||
</List>
|
</List>
|
||||||
</AppModal>
|
</AppModal>
|
||||||
) : (
|
) : (
|
||||||
@@ -556,7 +675,9 @@ const ChoreActionMenu = ({
|
|||||||
left: '50%',
|
left: '50%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showProjectPicker ? (
|
{showPriorityPicker ? (
|
||||||
|
renderMenuPriorityPicker()
|
||||||
|
) : showProjectPicker ? (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
|
|||||||
Reference in New Issue
Block a user