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,
|
||||
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) => {
|
||||
if (filter) {
|
||||
chores = chores.filter(chore => filter(chore))
|
||||
@@ -34,7 +95,7 @@ export const ChoresGrouper = (groupBy, chores, filter) => {
|
||||
chores.sort(ChoreSorter)
|
||||
var groups = []
|
||||
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:
|
||||
var groupRaw = {
|
||||
PendingApproval: [],
|
||||
@@ -147,82 +208,21 @@ export const ChoresGrouper = (groupBy, chores, filter) => {
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'due_date':
|
||||
var groupRaw = {
|
||||
Today: [],
|
||||
Tomorrow: [],
|
||||
'Next 7 Days': [],
|
||||
'Later This Month': [],
|
||||
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,
|
||||
},
|
||||
{
|
||||
case 'due_date': {
|
||||
var { dateGroups: dueDateGroups, anytime: dueAnytime } =
|
||||
buildActualDateGroups(chores)
|
||||
groups = [...dueDateGroups]
|
||||
if (dueAnytime.length > 0) {
|
||||
groups.push({
|
||||
name: 'Anytime',
|
||||
content: groupRaw['Anytime'],
|
||||
content: dueAnytime,
|
||||
color: TASK_COLOR.ANYTIME,
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'priority':
|
||||
groupRaw = {
|
||||
p1: [],
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
Typography,
|
||||
IconButton,
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -116,6 +116,8 @@ const CustomFilterChips = ({
|
||||
px: 1.0,
|
||||
py: 0.5,
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
opacity: hasWarning ? 0.7 : isActive ? 1 : 0.85,
|
||||
...(hasCustomColor && {
|
||||
@@ -185,6 +187,10 @@ const CustomFilterChips = ({
|
||||
maxWidth: 100,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
lineHeight: 1,
|
||||
...(hasCustomColor && {
|
||||
color: textColor,
|
||||
}),
|
||||
|
||||
@@ -9,12 +9,10 @@ const MyChoreHeader = ({
|
||||
tempFilter,
|
||||
tempFilterMeta,
|
||||
}) => {
|
||||
if (
|
||||
!activeFilterId &&
|
||||
!tempFilter &&
|
||||
(!selectedProject || selectedProject.id === 'default')
|
||||
)
|
||||
return null
|
||||
const isVisible =
|
||||
!!activeFilterId ||
|
||||
!!tempFilter ||
|
||||
(!!selectedProject && selectedProject.id !== 'default')
|
||||
|
||||
const renderIcon = () => {
|
||||
if (tempFilter) {
|
||||
@@ -53,18 +51,41 @@ const MyChoreHeader = ({
|
||||
: activeFilter?.description || selectedProject?.description
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
{renderIcon()}
|
||||
<Stack sx={{ flex: 1 }}>
|
||||
<Typography level='h3' sx={{ fontWeight: 'lg', color: 'text.primary' }}>
|
||||
{name}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
{description}
|
||||
<Box
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
maxHeight: isVisible ? '120px' : '0',
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transform: isVisible ? 'translateY(0)' : 'translateY(-8px)',
|
||||
transition:
|
||||
'max-height 0.3s ease-in-out, opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
|
||||
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>
|
||||
)}
|
||||
</Stack>
|
||||
<Box
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
RejectChore,
|
||||
SaveChore,
|
||||
SkipChore,
|
||||
UndoChoreAction,
|
||||
UpdateChoreAssignee,
|
||||
@@ -664,6 +665,28 @@ export const useChoreActions = ({
|
||||
}
|
||||
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 'completeWithPastDate':
|
||||
case 'changeAssignee':
|
||||
|
||||
@@ -1,52 +1,226 @@
|
||||
import { HomeRounded, Login } from '@mui/icons-material'
|
||||
import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy'
|
||||
import { Link } from 'react-router-dom'
|
||||
import Logo from '../Logo' // Adjust the import path as necessary
|
||||
import {
|
||||
BugReportRounded,
|
||||
CloudOffRounded,
|
||||
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 = 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 (
|
||||
<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
|
||||
className='flex flex-col items-center justify-center'
|
||||
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
|
||||
className='flex items-center gap-2'
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontSize: 24,
|
||||
mt: 2,
|
||||
width: 172,
|
||||
height: 172,
|
||||
borderRadius: '50%',
|
||||
border: '1.5px solid',
|
||||
borderColor: `${color}.100`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
Ops, something went wrong
|
||||
</Box>
|
||||
<Typography level='body-md' fontWeight={500} textAlign={'center'}>
|
||||
if you think this is a mistake, please contact us or{' '}
|
||||
<a
|
||||
href='https://github.com/donetick/donetick/issues/new'
|
||||
style={{
|
||||
textDecoration: 'underline',
|
||||
<Box
|
||||
sx={{
|
||||
width: 128,
|
||||
height: 128,
|
||||
borderRadius: '50%',
|
||||
border: '1.5px solid',
|
||||
borderColor: `${color}.200`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
open issue here
|
||||
</a>{' '}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
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
|
||||
component={Link}
|
||||
to='/chores'
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ mt: 4 }}
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
size='lg'
|
||||
startDecorator={<HomeRounded />}
|
||||
>
|
||||
@@ -55,16 +229,108 @@ const Error = () => {
|
||||
<Button
|
||||
component={Link}
|
||||
to='/login'
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
sx={{ mt: 1 }}
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
size='lg'
|
||||
startDecorator={<Login />}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</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 {
|
||||
Archive,
|
||||
ArrowBack,
|
||||
Cancel,
|
||||
CopyAll,
|
||||
Delete,
|
||||
DriveFileMove,
|
||||
Edit,
|
||||
ManageSearch,
|
||||
MoreTime,
|
||||
@@ -20,10 +22,25 @@ import {
|
||||
WbSunny,
|
||||
Weekend,
|
||||
} 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 { useNavigate } from 'react-router-dom'
|
||||
import LABEL_COLORS, {
|
||||
getTextColorFromBackgroundColor,
|
||||
} from '../../utils/Colors'
|
||||
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
|
||||
import { getIconComponent } from '../../utils/ProjectIcons'
|
||||
import { useProjects } from '../Projects/ProjectQueries'
|
||||
|
||||
const ChoreActionMenu = ({
|
||||
chore,
|
||||
@@ -43,10 +60,11 @@ const ChoreActionMenu = ({
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState(null)
|
||||
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
|
||||
const [showProjectPicker, setShowProjectPicker] = useState(false)
|
||||
const menuRef = React.useRef(null)
|
||||
const navigate = useNavigate()
|
||||
const { data: projects = [] } = useProjects()
|
||||
|
||||
// Check if this is the official donetick.com instance
|
||||
useEffect(() => {
|
||||
try {
|
||||
setIsOfficialInstance(isOfficialDonetickInstanceSync())
|
||||
@@ -83,6 +101,12 @@ const ChoreActionMenu = ({
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null)
|
||||
setShowProjectPicker(false)
|
||||
}
|
||||
|
||||
const handleMoveToProject = project => {
|
||||
onAction?.('moveToProject', chore, { project })
|
||||
handleMenuClose()
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
@@ -134,7 +158,6 @@ const ChoreActionMenu = ({
|
||||
|
||||
switch (option) {
|
||||
case 'today': {
|
||||
// Schedule for today at the next available slot: 9am, 12pm, 5pm, or now if after 5pm
|
||||
const nowHour = now.getHours()
|
||||
const scheduled = new Date(today)
|
||||
if (nowHour < 9) {
|
||||
@@ -144,7 +167,6 @@ const ChoreActionMenu = ({
|
||||
} else if (nowHour < 17) {
|
||||
scheduled.setHours(17, 0, 0, 0)
|
||||
} else {
|
||||
// After 5pm, use current time
|
||||
scheduled.setHours(
|
||||
now.getHours(),
|
||||
now.getMinutes(),
|
||||
@@ -163,7 +185,7 @@ const ChoreActionMenu = ({
|
||||
case 'tomorrow': {
|
||||
const tomorrow = new Date(today)
|
||||
tomorrow.setDate(today.getDate() + 1)
|
||||
tomorrow.setHours(12, 0, 0, 0) // Set to noon
|
||||
tomorrow.setHours(12, 0, 0, 0)
|
||||
return tomorrow
|
||||
}
|
||||
case 'tomorrow-afternoon': {
|
||||
@@ -195,6 +217,18 @@ const ChoreActionMenu = ({
|
||||
handleMenuClose()
|
||||
}
|
||||
|
||||
const renderProjectAvatar = (color, icon) => {
|
||||
const bg = color || LABEL_COLORS[0].value
|
||||
const IconComponent = getIconComponent(icon || 'FolderOpen')
|
||||
return (
|
||||
<Avatar size='sm' sx={{ width: 22, height: 22, backgroundColor: bg }}>
|
||||
<IconComponent
|
||||
sx={{ fontSize: 13, color: getTextColorFromBackgroundColor(bg) }}
|
||||
/>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -227,218 +261,267 @@ const ChoreActionMenu = ({
|
||||
left: '50%',
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onCompleteWithNote?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<NoteAdd />
|
||||
Complete with note
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onCompleteWithPastDate?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Update />
|
||||
Complete in past
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleSkip()
|
||||
}}
|
||||
>
|
||||
<SwitchAccessShortcut />
|
||||
Skip to next due date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onChangeAssignee?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<RecordVoiceOver />
|
||||
Delegate to someone else
|
||||
</MenuItem>
|
||||
{isOfficialInstance && (
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onNudge?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Notifications />
|
||||
Send nudge
|
||||
</MenuItem>
|
||||
)}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleHistory()
|
||||
}}
|
||||
>
|
||||
<ManageSearch />
|
||||
History
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
cursor: 'default',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<Tooltip title='Today' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
{showProjectPicker ? (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('today')
|
||||
setShowProjectPicker(false)
|
||||
}}
|
||||
sx={{ gap: 1 }}
|
||||
>
|
||||
<Today />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='Tomorrow' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
<ArrowBack fontSize='small' />
|
||||
<Typography level='body-sm' fontWeight={600}>
|
||||
Move to project
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('tomorrow')
|
||||
handleMoveToProject({ id: null, name: 'Default Project' })
|
||||
}}
|
||||
>
|
||||
<WbSunny />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/* <Tooltip title='Tomorrow afternoon' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
<ListItemDecorator>
|
||||
{renderProjectAvatar(LABEL_COLORS[0].value, 'FolderOpen')}
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography level='body-sm'>Default Project</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
{projects.map(project => (
|
||||
<MenuItem
|
||||
key={project.id}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleMoveToProject(project)
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
{renderProjectAvatar(project.color, project.icon)}
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography level='body-sm'>{project.name}</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('tomorrow-afternoon')
|
||||
onCompleteWithNote?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<WbTwilight />
|
||||
</IconButton>
|
||||
</Tooltip> */}
|
||||
<Tooltip title='Weekend' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
<NoteAdd />
|
||||
Complete with note
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('weekend')
|
||||
onCompleteWithPastDate?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Weekend />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='Next week' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
<Update />
|
||||
Complete in past
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('next-week')
|
||||
handleSkip()
|
||||
}}
|
||||
>
|
||||
<NextWeek />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='Remove due date' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
<SwitchAccessShortcut />
|
||||
Skip to next due date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onChangeAssignee?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<RecordVoiceOver />
|
||||
Delegate to someone else
|
||||
</MenuItem>
|
||||
{isOfficialInstance && (
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onNudge?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Notifications />
|
||||
Send nudge
|
||||
</MenuItem>
|
||||
)}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleHistory()
|
||||
}}
|
||||
>
|
||||
<ManageSearch />
|
||||
History
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
cursor: 'default',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<Tooltip title='Today' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('today')
|
||||
}}
|
||||
>
|
||||
<Today />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='Tomorrow' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('tomorrow')
|
||||
}}
|
||||
>
|
||||
<WbSunny />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='Weekend' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('weekend')
|
||||
}}
|
||||
>
|
||||
<Weekend />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='Next week' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('next-week')
|
||||
}}
|
||||
>
|
||||
<NextWeek />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='Remove due date' placement='top'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
color='neutral'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('remove')
|
||||
}}
|
||||
>
|
||||
<Cancel />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onChangeDueDate?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<MoreTime />
|
||||
Change due date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onWriteNFC?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Nfc />
|
||||
Write to NFC
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleEdit()
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleClone()
|
||||
}}
|
||||
>
|
||||
<CopyAll />
|
||||
Clone
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleView()
|
||||
}}
|
||||
>
|
||||
<ViewCarousel />
|
||||
View
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleArchive()
|
||||
}}
|
||||
color='neutral'
|
||||
>
|
||||
{chore.isActive ? <Archive /> : <Unarchive />}
|
||||
{chore.isActive ? 'Archive' : 'Unarchive'}
|
||||
</MenuItem>
|
||||
{projects.length > 0 && (
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
setShowProjectPicker(true)
|
||||
}}
|
||||
>
|
||||
<DriveFileMove />
|
||||
Move to project
|
||||
</MenuItem>
|
||||
)}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleQuickSchedule('remove')
|
||||
handleDelete()
|
||||
}}
|
||||
color='danger'
|
||||
>
|
||||
<Cancel />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onChangeDueDate?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<MoreTime />
|
||||
Change due date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onWriteNFC?.()
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Nfc />
|
||||
Write to NFC
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleEdit()
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleClone()
|
||||
}}
|
||||
>
|
||||
<CopyAll />
|
||||
Clone
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleView()
|
||||
}}
|
||||
>
|
||||
<ViewCarousel />
|
||||
View
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleArchive()
|
||||
}}
|
||||
color='neutral'
|
||||
>
|
||||
{chore.isActive ? <Archive /> : <Unarchive />}
|
||||
{chore.isActive ? 'Archive' : 'Unarchive'}
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleDelete()
|
||||
}}
|
||||
color='danger'
|
||||
>
|
||||
<Delete />
|
||||
Delete
|
||||
</MenuItem>
|
||||
<Delete />
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user