Merge pull request #128 from donetick/ux-improvment-07-01-2026
Ux improvment 07 01 2026
This commit is contained in:
@@ -25,6 +25,67 @@ export const ChoreStatus = Object.freeze({
|
|||||||
PAUSED: 2,
|
PAUSED: 2,
|
||||||
PENDING_APPROVAL: 3,
|
PENDING_APPROVAL: 3,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getDateGroupKey = dueDate =>
|
||||||
|
moment(dueDate).startOf('day').format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
const getDateGroupName = dateKey =>
|
||||||
|
moment(dateKey, 'YYYY-MM-DD').format('dddd, MMM D')
|
||||||
|
|
||||||
|
const getDateGroupColor = dateKey => {
|
||||||
|
const today = moment().startOf('day')
|
||||||
|
const tomorrow = moment().add(1, 'day').startOf('day')
|
||||||
|
const groupDate = moment(dateKey, 'YYYY-MM-DD')
|
||||||
|
|
||||||
|
if (groupDate.isBefore(today)) {
|
||||||
|
return TASK_COLOR.OVERDUE
|
||||||
|
}
|
||||||
|
if (groupDate.isSame(today)) {
|
||||||
|
return TASK_COLOR.TODAY
|
||||||
|
}
|
||||||
|
if (groupDate.isSame(tomorrow)) {
|
||||||
|
return TASK_COLOR.TOMORROW
|
||||||
|
}
|
||||||
|
if (groupDate.isBefore(moment(today).add(8, 'days'))) {
|
||||||
|
return TASK_COLOR.NEXT_7_DAYS
|
||||||
|
}
|
||||||
|
if (groupDate.isSame(today, 'month')) {
|
||||||
|
return TASK_COLOR.LATER_THIS_MONTH
|
||||||
|
}
|
||||||
|
return TASK_COLOR.FUTURE
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildActualDateGroups = chores => {
|
||||||
|
const groupedByDate = {}
|
||||||
|
const anytime = []
|
||||||
|
|
||||||
|
chores.forEach(chore => {
|
||||||
|
if (!chore.nextDueDate) {
|
||||||
|
anytime.push(chore)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateKey = getDateGroupKey(chore.nextDueDate)
|
||||||
|
if (!groupedByDate[dateKey]) {
|
||||||
|
groupedByDate[dateKey] = []
|
||||||
|
}
|
||||||
|
groupedByDate[dateKey].push(chore)
|
||||||
|
})
|
||||||
|
|
||||||
|
const dateGroups = Object.keys(groupedByDate)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
moment(a, 'YYYY-MM-DD').valueOf() - moment(b, 'YYYY-MM-DD').valueOf(),
|
||||||
|
)
|
||||||
|
.map(dateKey => ({
|
||||||
|
name: getDateGroupName(dateKey),
|
||||||
|
content: groupedByDate[dateKey],
|
||||||
|
color: getDateGroupColor(dateKey),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return { dateGroups, anytime }
|
||||||
|
}
|
||||||
|
|
||||||
export const ChoresGrouper = (groupBy, chores, filter) => {
|
export const ChoresGrouper = (groupBy, chores, filter) => {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
chores = chores.filter(chore => filter(chore))
|
chores = chores.filter(chore => filter(chore))
|
||||||
@@ -34,7 +95,7 @@ export const ChoresGrouper = (groupBy, chores, filter) => {
|
|||||||
chores.sort(ChoreSorter)
|
chores.sort(ChoreSorter)
|
||||||
var groups = []
|
var groups = []
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case 'default':
|
case 'default': {
|
||||||
// same as due_date but hide empty groups: and if status is 1 or 2 have seperated catigory as Started:
|
// same as due_date but hide empty groups: and if status is 1 or 2 have seperated catigory as Started:
|
||||||
var groupRaw = {
|
var groupRaw = {
|
||||||
PendingApproval: [],
|
PendingApproval: [],
|
||||||
@@ -147,82 +208,21 @@ export const ChoresGrouper = (groupBy, chores, filter) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'due_date':
|
case 'due_date': {
|
||||||
var groupRaw = {
|
var { dateGroups: dueDateGroups, anytime: dueAnytime } =
|
||||||
Today: [],
|
buildActualDateGroups(chores)
|
||||||
Tomorrow: [],
|
groups = [...dueDateGroups]
|
||||||
'Next 7 Days': [],
|
if (dueAnytime.length > 0) {
|
||||||
'Later This Month': [],
|
groups.push({
|
||||||
Future: [],
|
|
||||||
Overdue: [],
|
|
||||||
Anytime: [],
|
|
||||||
}
|
|
||||||
chores.forEach(chore => {
|
|
||||||
if (chore.nextDueDate === null) {
|
|
||||||
groupRaw['Anytime'].push(chore)
|
|
||||||
} else if (new Date(chore.nextDueDate) < new Date()) {
|
|
||||||
groupRaw['Overdue'].push(chore)
|
|
||||||
} else if (
|
|
||||||
new Date(chore.nextDueDate).toDateString() ===
|
|
||||||
new Date().toDateString()
|
|
||||||
) {
|
|
||||||
groupRaw['Today'].push(chore)
|
|
||||||
} else if (
|
|
||||||
new Date(chore.nextDueDate).toDateString() ===
|
|
||||||
new Date(Date.now() + 24 * 60 * 60 * 1000).toDateString()
|
|
||||||
) {
|
|
||||||
groupRaw['Tomorrow'].push(chore)
|
|
||||||
} else if (
|
|
||||||
new Date(chore.nextDueDate) <
|
|
||||||
new Date(Date.now() + 8 * 24 * 60 * 60 * 1000) &&
|
|
||||||
new Date(chore.nextDueDate) >
|
|
||||||
new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
||||||
) {
|
|
||||||
groupRaw['Next 7 Days'].push(chore)
|
|
||||||
} else if (
|
|
||||||
new Date(chore.nextDueDate).getMonth() === new Date().getMonth() &&
|
|
||||||
new Date(chore.nextDueDate).getFullYear() === new Date().getFullYear()
|
|
||||||
) {
|
|
||||||
groupRaw['Later This Month'].push(chore)
|
|
||||||
} else {
|
|
||||||
groupRaw['Future'].push(chore)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
groups = [
|
|
||||||
{
|
|
||||||
name: 'Overdue',
|
|
||||||
content: groupRaw['Overdue'],
|
|
||||||
color: TASK_COLOR.OVERDUE,
|
|
||||||
},
|
|
||||||
{ name: 'Today', content: groupRaw['Today'], color: TASK_COLOR.TODAY },
|
|
||||||
{
|
|
||||||
name: 'Tomorrow',
|
|
||||||
content: groupRaw['Tomorrow'],
|
|
||||||
color: TASK_COLOR.TOMORROW,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Next 7 Days',
|
|
||||||
content: groupRaw['Next 7 Days'],
|
|
||||||
color: TASK_COLOR.NEXT_7_DAYS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Later This Month',
|
|
||||||
content: groupRaw['Later This Month'],
|
|
||||||
color: TASK_COLOR.LATER_THIS_MONTH,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Future',
|
|
||||||
content: groupRaw['Future'],
|
|
||||||
color: TASK_COLOR.FUTURE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Anytime',
|
name: 'Anytime',
|
||||||
content: groupRaw['Anytime'],
|
content: dueAnytime,
|
||||||
color: TASK_COLOR.ANYTIME,
|
color: TASK_COLOR.ANYTIME,
|
||||||
},
|
})
|
||||||
]
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case 'priority':
|
case 'priority':
|
||||||
groupRaw = {
|
groupRaw = {
|
||||||
p1: [],
|
p1: [],
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Chip,
|
Chip,
|
||||||
|
IconButton,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
IconButton,
|
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
@@ -116,6 +116,8 @@ const CustomFilterChips = ({
|
|||||||
px: 1.0,
|
px: 1.0,
|
||||||
py: 0.5,
|
py: 0.5,
|
||||||
height: 32,
|
height: 32,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
|
||||||
opacity: hasWarning ? 0.7 : isActive ? 1 : 0.85,
|
opacity: hasWarning ? 0.7 : isActive ? 1 : 0.85,
|
||||||
...(hasCustomColor && {
|
...(hasCustomColor && {
|
||||||
@@ -185,6 +187,10 @@ const CustomFilterChips = ({
|
|||||||
maxWidth: 100,
|
maxWidth: 100,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100%',
|
||||||
|
lineHeight: 1,
|
||||||
...(hasCustomColor && {
|
...(hasCustomColor && {
|
||||||
color: textColor,
|
color: textColor,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ const MyChoreHeader = ({
|
|||||||
tempFilter,
|
tempFilter,
|
||||||
tempFilterMeta,
|
tempFilterMeta,
|
||||||
}) => {
|
}) => {
|
||||||
if (
|
const isVisible =
|
||||||
!activeFilterId &&
|
!!activeFilterId ||
|
||||||
!tempFilter &&
|
!!tempFilter ||
|
||||||
(!selectedProject || selectedProject.id === 'default')
|
(!!selectedProject && selectedProject.id !== 'default')
|
||||||
)
|
|
||||||
return null
|
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (tempFilter) {
|
if (tempFilter) {
|
||||||
@@ -53,18 +51,41 @@ const MyChoreHeader = ({
|
|||||||
: activeFilter?.description || selectedProject?.description
|
: activeFilter?.description || selectedProject?.description
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
<Box
|
||||||
{renderIcon()}
|
sx={{
|
||||||
<Stack sx={{ flex: 1 }}>
|
overflow: 'hidden',
|
||||||
<Typography level='h3' sx={{ fontWeight: 'lg', color: 'text.primary' }}>
|
maxHeight: isVisible ? '120px' : '0',
|
||||||
{name}
|
opacity: isVisible ? 1 : 0,
|
||||||
</Typography>
|
transform: isVisible ? 'translateY(0)' : 'translateY(-8px)',
|
||||||
{description && (
|
transition:
|
||||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
'max-height 0.3s ease-in-out, opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
|
||||||
{description}
|
marginBottom: isVisible ? 2 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
{renderIcon()}
|
||||||
|
<Stack sx={{ flex: 1 }}>
|
||||||
|
<Typography
|
||||||
|
level='h3'
|
||||||
|
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
<Box
|
||||||
</Stack>
|
sx={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
maxHeight: description ? '40px' : '0',
|
||||||
|
opacity: description ? 1 : 0,
|
||||||
|
transition:
|
||||||
|
'max-height 0.3s ease-in-out, opacity 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
MarkChoreComplete,
|
MarkChoreComplete,
|
||||||
NudgeChore,
|
NudgeChore,
|
||||||
RejectChore,
|
RejectChore,
|
||||||
|
SaveChore,
|
||||||
SkipChore,
|
SkipChore,
|
||||||
UndoChoreAction,
|
UndoChoreAction,
|
||||||
UpdateChoreAssignee,
|
UpdateChoreAssignee,
|
||||||
@@ -664,6 +665,28 @@ export const useChoreActions = ({
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'moveToProject': {
|
||||||
|
const project = extraData?.project
|
||||||
|
const projectId = project?.id === null ? null : project?.id
|
||||||
|
const updatedChore = { ...chore, projectId }
|
||||||
|
try {
|
||||||
|
const response = await SaveChore(updatedChore)
|
||||||
|
if (response.ok) {
|
||||||
|
updateChoreInState(updatedChore, 'moved-to-project')
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'completeWithNote':
|
case 'completeWithNote':
|
||||||
case 'completeWithPastDate':
|
case 'completeWithPastDate':
|
||||||
case 'changeAssignee':
|
case 'changeAssignee':
|
||||||
|
|||||||
@@ -1,52 +1,226 @@
|
|||||||
import { HomeRounded, Login } from '@mui/icons-material'
|
import {
|
||||||
import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy'
|
BugReportRounded,
|
||||||
import { Link } from 'react-router-dom'
|
CloudOffRounded,
|
||||||
import Logo from '../Logo' // Adjust the import path as necessary
|
ContentCopyRounded,
|
||||||
|
ErrorRounded,
|
||||||
|
ExpandMoreRounded,
|
||||||
|
HomeRounded,
|
||||||
|
LockRounded,
|
||||||
|
RefreshRounded,
|
||||||
|
SearchOffRounded,
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
import { Box, Button, IconButton, Snackbar, Typography } from '@mui/joy'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Link, useRouteError } from 'react-router-dom'
|
||||||
|
|
||||||
|
const getErrorKind = error => {
|
||||||
|
if (!error)
|
||||||
|
return { label: 'Unknown Error', color: 'danger', Icon: ErrorRounded }
|
||||||
|
const status = error?.status ?? error?.response?.status
|
||||||
|
if (status === 404)
|
||||||
|
return {
|
||||||
|
label: '404 · Not Found',
|
||||||
|
color: 'warning',
|
||||||
|
Icon: SearchOffRounded,
|
||||||
|
}
|
||||||
|
if (status === 401 || status === 403)
|
||||||
|
return {
|
||||||
|
label: `${status} · Unauthorized`,
|
||||||
|
color: 'warning',
|
||||||
|
Icon: LockRounded,
|
||||||
|
}
|
||||||
|
if (status >= 500)
|
||||||
|
return {
|
||||||
|
label: `${status} · Server Error`,
|
||||||
|
color: 'danger',
|
||||||
|
Icon: CloudOffRounded,
|
||||||
|
}
|
||||||
|
if (error?.name === 'TypeError')
|
||||||
|
return { label: 'Runtime Error', color: 'danger', Icon: BugReportRounded }
|
||||||
|
if (error?.name === 'SyntaxError')
|
||||||
|
return { label: 'Syntax Error', color: 'danger', Icon: BugReportRounded }
|
||||||
|
return { label: 'Unexpected Error', color: 'danger', Icon: ErrorRounded }
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeMessage = error => {
|
||||||
|
const msg = error?.message ?? error?.statusText
|
||||||
|
if (!msg || msg === '[object Object]') return null
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildErrorText = (error, url) => {
|
||||||
|
const lines = [
|
||||||
|
`URL: ${url}`,
|
||||||
|
`Time: ${new Date().toISOString()}`,
|
||||||
|
`Error: ${safeMessage(error) ?? String(error)}`,
|
||||||
|
]
|
||||||
|
if (error?.stack) lines.push(`\nStack:\n${error.stack}`)
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
const Error = () => {
|
const Error = () => {
|
||||||
|
const error = useRouteError()
|
||||||
|
const [showDetails, setShowDetails] = useState(false)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
const { color, Icon } = getErrorKind(error)
|
||||||
|
const message = safeMessage(error)
|
||||||
|
const url = window.location.href
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(buildErrorText(error, url)).then(() => {
|
||||||
|
setCopied(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className='flex h-full items-center justify-center'>
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: '100dvh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
px: 3,
|
||||||
|
py: 6,
|
||||||
|
maxWidth: 440,
|
||||||
|
mx: 'auto',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Decorative dots */}
|
||||||
<Box
|
<Box
|
||||||
className='flex flex-col items-center justify-center'
|
|
||||||
sx={{
|
sx={{
|
||||||
minHeight: '80vh',
|
position: 'absolute',
|
||||||
|
top: '14%',
|
||||||
|
left: '6%',
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: `${color}.100`,
|
||||||
|
opacity: 0.7,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '22%',
|
||||||
|
right: '8%',
|
||||||
|
width: 9,
|
||||||
|
height: 9,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: `${color}.200`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '28%',
|
||||||
|
right: '6%',
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: `${color}.100`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Icon with concentric rings */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
mb: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress
|
|
||||||
value={100}
|
|
||||||
color='danger' // Set the color to 'error' for danger color
|
|
||||||
sx={{ '--CircularProgress-size': '200px' }}
|
|
||||||
>
|
|
||||||
<Logo />
|
|
||||||
</CircularProgress>
|
|
||||||
<Box
|
<Box
|
||||||
className='flex items-center gap-2'
|
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 700,
|
width: 172,
|
||||||
fontSize: 24,
|
height: 172,
|
||||||
mt: 2,
|
borderRadius: '50%',
|
||||||
|
border: '1.5px solid',
|
||||||
|
borderColor: `${color}.100`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Ops, something went wrong
|
<Box
|
||||||
</Box>
|
sx={{
|
||||||
<Typography level='body-md' fontWeight={500} textAlign={'center'}>
|
width: 128,
|
||||||
if you think this is a mistake, please contact us or{' '}
|
height: 128,
|
||||||
<a
|
borderRadius: '50%',
|
||||||
href='https://github.com/donetick/donetick/issues/new'
|
border: '1.5px solid',
|
||||||
style={{
|
borderColor: `${color}.200`,
|
||||||
textDecoration: 'underline',
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
open issue here
|
<Box
|
||||||
</a>{' '}
|
sx={{
|
||||||
</Typography>
|
width: 84,
|
||||||
|
height: 84,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: `${color}.50`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon sx={{ fontSize: 42, color: `${color}.500` }} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<Typography
|
||||||
|
level='h3'
|
||||||
|
fontWeight={700}
|
||||||
|
textAlign='center'
|
||||||
|
sx={{ mb: 1.5 }}
|
||||||
|
>
|
||||||
|
Something went wrong
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Error message */}
|
||||||
|
<Typography
|
||||||
|
level='body-sm'
|
||||||
|
textAlign='center'
|
||||||
|
sx={{
|
||||||
|
color: 'text.secondary',
|
||||||
|
mb: 4,
|
||||||
|
maxWidth: 320,
|
||||||
|
minHeight: '2.5em',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message ??
|
||||||
|
'An unexpected error occurred. Try reloading — it usually fixes it.'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Primary CTA */}
|
||||||
|
<Button
|
||||||
|
variant='solid'
|
||||||
|
color='primary'
|
||||||
|
size='lg'
|
||||||
|
startDecorator={<RefreshRounded />}
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
sx={{ width: '100%', mb: 2 }}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Secondary actions */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 3, mb: 5 }}>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to='/chores'
|
to='/chores'
|
||||||
variant='outlined'
|
variant='plain'
|
||||||
color='primary'
|
color='neutral'
|
||||||
sx={{ mt: 4 }}
|
|
||||||
size='lg'
|
size='lg'
|
||||||
startDecorator={<HomeRounded />}
|
startDecorator={<HomeRounded />}
|
||||||
>
|
>
|
||||||
@@ -55,16 +229,108 @@ const Error = () => {
|
|||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to='/login'
|
to='/login'
|
||||||
variant='outlined'
|
variant='plain'
|
||||||
color='primary'
|
color='neutral'
|
||||||
sx={{ mt: 1 }}
|
|
||||||
size='lg'
|
size='lg'
|
||||||
startDecorator={<Login />}
|
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
|
||||||
|
{/* Report hint + collapsible details */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
level='body-xs'
|
||||||
|
textAlign='center'
|
||||||
|
sx={{ color: 'text.tertiary', mb: 1.5 }}
|
||||||
|
>
|
||||||
|
If this keeps happening,{' '}
|
||||||
|
<a
|
||||||
|
href='https://github.com/donetick/donetick/issues/new'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
open an issue
|
||||||
|
</a>{' '}
|
||||||
|
and include the error details below.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{(error?.stack || message) && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant='plain'
|
||||||
|
color='neutral'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => setShowDetails(v => !v)}
|
||||||
|
endDecorator={
|
||||||
|
<ExpandMoreRounded
|
||||||
|
sx={{
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
transform: showDetails ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
>
|
||||||
|
{showDetails ? 'Hide' : 'Show'} error details
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showDetails && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
bgcolor: 'background.level2',
|
||||||
|
borderRadius: 'sm',
|
||||||
|
p: 2,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size='sm'
|
||||||
|
variant='plain'
|
||||||
|
color='neutral'
|
||||||
|
onClick={handleCopy}
|
||||||
|
sx={{ position: 'absolute', top: 8, right: 8 }}
|
||||||
|
title='Copy to clipboard'
|
||||||
|
>
|
||||||
|
<ContentCopyRounded fontSize='small' />
|
||||||
|
</IconButton>
|
||||||
|
<Typography
|
||||||
|
level='body-xs'
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
pr: 4,
|
||||||
|
color: 'text.secondary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{buildErrorText(error, url)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={copied}
|
||||||
|
autoHideDuration={2500}
|
||||||
|
onClose={() => setCopied(false)}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
Error details copied to clipboard
|
||||||
|
</Snackbar>
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Archive,
|
Archive,
|
||||||
|
ArrowBack,
|
||||||
Cancel,
|
Cancel,
|
||||||
CopyAll,
|
CopyAll,
|
||||||
Delete,
|
Delete,
|
||||||
|
DriveFileMove,
|
||||||
Edit,
|
Edit,
|
||||||
ManageSearch,
|
ManageSearch,
|
||||||
MoreTime,
|
MoreTime,
|
||||||
@@ -20,10 +22,25 @@ import {
|
|||||||
WbSunny,
|
WbSunny,
|
||||||
Weekend,
|
Weekend,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { Divider, IconButton, Menu, MenuItem, Tooltip } from '@mui/joy'
|
import {
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
ListItemContent,
|
||||||
|
ListItemDecorator,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import LABEL_COLORS, {
|
||||||
|
getTextColorFromBackgroundColor,
|
||||||
|
} from '../../utils/Colors'
|
||||||
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
|
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
|
||||||
|
import { getIconComponent } from '../../utils/ProjectIcons'
|
||||||
|
import { useProjects } from '../Projects/ProjectQueries'
|
||||||
|
|
||||||
const ChoreActionMenu = ({
|
const ChoreActionMenu = ({
|
||||||
chore,
|
chore,
|
||||||
@@ -43,10 +60,11 @@ const ChoreActionMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
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 menuRef = React.useRef(null)
|
const menuRef = React.useRef(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { data: projects = [] } = useProjects()
|
||||||
|
|
||||||
// Check if this is the official donetick.com instance
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
setIsOfficialInstance(isOfficialDonetickInstanceSync())
|
setIsOfficialInstance(isOfficialDonetickInstanceSync())
|
||||||
@@ -83,6 +101,12 @@ const ChoreActionMenu = ({
|
|||||||
|
|
||||||
const handleMenuClose = () => {
|
const handleMenuClose = () => {
|
||||||
setAnchorEl(null)
|
setAnchorEl(null)
|
||||||
|
setShowProjectPicker(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMoveToProject = project => {
|
||||||
|
onAction?.('moveToProject', chore, { project })
|
||||||
|
handleMenuClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
@@ -134,7 +158,6 @@ const ChoreActionMenu = ({
|
|||||||
|
|
||||||
switch (option) {
|
switch (option) {
|
||||||
case 'today': {
|
case 'today': {
|
||||||
// Schedule for today at the next available slot: 9am, 12pm, 5pm, or now if after 5pm
|
|
||||||
const nowHour = now.getHours()
|
const nowHour = now.getHours()
|
||||||
const scheduled = new Date(today)
|
const scheduled = new Date(today)
|
||||||
if (nowHour < 9) {
|
if (nowHour < 9) {
|
||||||
@@ -144,7 +167,6 @@ const ChoreActionMenu = ({
|
|||||||
} else if (nowHour < 17) {
|
} else if (nowHour < 17) {
|
||||||
scheduled.setHours(17, 0, 0, 0)
|
scheduled.setHours(17, 0, 0, 0)
|
||||||
} else {
|
} else {
|
||||||
// After 5pm, use current time
|
|
||||||
scheduled.setHours(
|
scheduled.setHours(
|
||||||
now.getHours(),
|
now.getHours(),
|
||||||
now.getMinutes(),
|
now.getMinutes(),
|
||||||
@@ -163,7 +185,7 @@ const ChoreActionMenu = ({
|
|||||||
case 'tomorrow': {
|
case 'tomorrow': {
|
||||||
const tomorrow = new Date(today)
|
const tomorrow = new Date(today)
|
||||||
tomorrow.setDate(today.getDate() + 1)
|
tomorrow.setDate(today.getDate() + 1)
|
||||||
tomorrow.setHours(12, 0, 0, 0) // Set to noon
|
tomorrow.setHours(12, 0, 0, 0)
|
||||||
return tomorrow
|
return tomorrow
|
||||||
}
|
}
|
||||||
case 'tomorrow-afternoon': {
|
case 'tomorrow-afternoon': {
|
||||||
@@ -195,6 +217,18 @@ const ChoreActionMenu = ({
|
|||||||
handleMenuClose()
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -227,218 +261,267 @@ const ChoreActionMenu = ({
|
|||||||
left: '50%',
|
left: '50%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem
|
{showProjectPicker ? (
|
||||||
onClick={e => {
|
<>
|
||||||
e.stopPropagation()
|
<MenuItem
|
||||||
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 => {
|
onClick={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleQuickSchedule('today')
|
setShowProjectPicker(false)
|
||||||
}}
|
}}
|
||||||
|
sx={{ gap: 1 }}
|
||||||
>
|
>
|
||||||
<Today />
|
<ArrowBack fontSize='small' />
|
||||||
</IconButton>
|
<Typography level='body-sm' fontWeight={600}>
|
||||||
</Tooltip>
|
Move to project
|
||||||
<Tooltip title='Tomorrow' placement='top'>
|
</Typography>
|
||||||
<IconButton
|
</MenuItem>
|
||||||
size='sm'
|
<Divider />
|
||||||
|
<MenuItem
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleQuickSchedule('tomorrow')
|
handleMoveToProject({ id: null, name: 'Default Project' })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WbSunny />
|
<ListItemDecorator>
|
||||||
</IconButton>
|
{renderProjectAvatar(LABEL_COLORS[0].value, 'FolderOpen')}
|
||||||
</Tooltip>
|
</ListItemDecorator>
|
||||||
{/* <Tooltip title='Tomorrow afternoon' placement='top'>
|
<ListItemContent>
|
||||||
<IconButton
|
<Typography level='body-sm'>Default Project</Typography>
|
||||||
size='sm'
|
</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 => {
|
onClick={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleQuickSchedule('tomorrow-afternoon')
|
onCompleteWithNote?.()
|
||||||
|
handleMenuClose()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WbTwilight />
|
<NoteAdd />
|
||||||
</IconButton>
|
Complete with note
|
||||||
</Tooltip> */}
|
</MenuItem>
|
||||||
<Tooltip title='Weekend' placement='top'>
|
<MenuItem
|
||||||
<IconButton
|
|
||||||
size='sm'
|
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleQuickSchedule('weekend')
|
onCompleteWithPastDate?.()
|
||||||
|
handleMenuClose()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Weekend />
|
<Update />
|
||||||
</IconButton>
|
Complete in past
|
||||||
</Tooltip>
|
</MenuItem>
|
||||||
<Tooltip title='Next week' placement='top'>
|
<MenuItem
|
||||||
<IconButton
|
|
||||||
size='sm'
|
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleQuickSchedule('next-week')
|
handleSkip()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NextWeek />
|
<SwitchAccessShortcut />
|
||||||
</IconButton>
|
Skip to next due date
|
||||||
</Tooltip>
|
</MenuItem>
|
||||||
<Tooltip title='Remove due date' placement='top'>
|
<MenuItem
|
||||||
<IconButton
|
onClick={e => {
|
||||||
size='sm'
|
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'
|
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 => {
|
onClick={e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleQuickSchedule('remove')
|
handleDelete()
|
||||||
}}
|
}}
|
||||||
|
color='danger'
|
||||||
>
|
>
|
||||||
<Cancel />
|
<Delete />
|
||||||
</IconButton>
|
Delete
|
||||||
</Tooltip>
|
</MenuItem>
|
||||||
</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>
|
|
||||||
<Divider />
|
|
||||||
<MenuItem
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation()
|
|
||||||
handleDelete()
|
|
||||||
}}
|
|
||||||
color='danger'
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
Delete
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user