Add Debounce, Add Description, fix TaskModal issue with every_other

This commit is contained in:
Mo Tarbin 2025-01-17 00:52:24 -05:00
commit dfa5763605
9 changed files with 178 additions and 33 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "donetick", "name": "donetick",
"private": true, "private": true,
"version": "0.1.90", "version": "0.1.91",
"type": "module", "type": "module",
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx}": [ "*.{js,jsx,ts,tsx}": [

19
src/utils/Debounce.jsx Normal file
View file

@ -0,0 +1,19 @@
import { useEffect, useState } from 'react'
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
export default useDebounce

View file

@ -22,6 +22,7 @@ import {
Snackbar, Snackbar,
Stack, Stack,
Switch, Switch,
Textarea,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import moment from 'moment' import moment from 'moment'
@ -61,6 +62,7 @@ const ChoreEdit = () => {
const [userHistory, setUserHistory] = useState({}) const [userHistory, setUserHistory] = useState({})
const { choreId } = useParams() const { choreId } = useParams()
const [name, setName] = useState('') const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [confirmModelConfig, setConfirmModelConfig] = useState({}) const [confirmModelConfig, setConfirmModelConfig] = useState({})
const [assignees, setAssignees] = useState([]) const [assignees, setAssignees] = useState([])
const [performers, setPerformers] = useState([]) const [performers, setPerformers] = useState([])
@ -186,6 +188,7 @@ const ChoreEdit = () => {
const chore = { const chore = {
id: Number(choreId), id: Number(choreId),
name: name, name: name,
description: description,
assignees: assignees, assignees: assignees,
dueDate: dueDate ? new Date(dueDate).toISOString() : null, dueDate: dueDate ? new Date(dueDate).toISOString() : null,
frequencyType: frequencyType, frequencyType: frequencyType,
@ -241,6 +244,7 @@ const ChoreEdit = () => {
.then(data => { .then(data => {
setChore(data.res) setChore(data.res)
setName(data.res.name ? data.res.name : '') setName(data.res.name ? data.res.name : '')
setDescription(data.res.description ? data.res.description : '')
setAssignees(data.res.assignees ? data.res.assignees : []) setAssignees(data.res.assignees ? data.res.assignees : [])
setAssignedTo(data.res.assignedTo) setAssignedTo(data.res.assignedTo)
setFrequencyType( setFrequencyType(
@ -379,11 +383,22 @@ const ChoreEdit = () => {
<Box> <Box>
<FormControl error={errors.name}> <FormControl error={errors.name}>
<Typography level='h4'>Title :</Typography> <Typography level='h4'>Title :</Typography>
<Typography level='h5'>What is this chore about?</Typography> <Typography level='h5'> What is the name of this chore?</Typography>
<Input value={name} onChange={e => setName(e.target.value)} /> <Input value={name} onChange={e => setName(e.target.value)} />
<FormHelperText error>{errors.name}</FormHelperText> <FormHelperText error>{errors.name}</FormHelperText>
</FormControl> </FormControl>
</Box> </Box>
<Box mt={2}>
<FormControl error={errors.description}>
<Typography level='h4'>Details:</Typography>
<Typography level='h5'>What is this chore about?</Typography>
<Textarea
value={description}
onChange={e => setDescription(e.target.value)}
/>
<FormHelperText error>{errors.name}</FormHelperText>
</FormControl>
</Box>
<Box mt={2}> <Box mt={2}>
<Typography level='h4'>Assignees :</Typography> <Typography level='h4'>Assignees :</Typography>
<Typography level='h5'>Who can do this chore?</Typography> <Typography level='h5'>Who can do this chore?</Typography>

View file

@ -3,9 +3,11 @@ import {
CancelScheduleSend, CancelScheduleSend,
Check, Check,
Checklist, Checklist,
CloseFullscreen,
Edit, Edit,
History, History,
LowPriority, LowPriority,
OpenInFull,
PeopleAlt, PeopleAlt,
Person, Person,
SwitchAccessShortcut, SwitchAccessShortcut,
@ -21,6 +23,7 @@ import {
Dropdown, Dropdown,
FormControl, FormControl,
Grid, Grid,
IconButton,
Input, Input,
ListItem, ListItem,
ListItemContent, ListItemContent,
@ -74,6 +77,7 @@ const ChoreView = () => {
const [completedDate, setCompletedDate] = useState(null) const [completedDate, setCompletedDate] = useState(null)
const [confirmModelConfig, setConfirmModelConfig] = useState({}) const [confirmModelConfig, setConfirmModelConfig] = useState({})
const [chorePriority, setChorePriority] = useState(null) const [chorePriority, setChorePriority] = useState(null)
const [isDescriptionOpen, setIsDescriptionOpen] = useState(false)
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
GetChoreDetailById(choreId).then(resp => { GetChoreDetailById(choreId).then(resp => {
@ -420,6 +424,47 @@ const ChoreView = () => {
</Button> </Button>
</Box> </Box>
{chore.description && (
<>
<Typography level='title-md' sx={{ mb: 1 }}>
Description :
</Typography>
<Sheet
variant='outlined'
sx={{
p: 2,
borderRadius: 'lg',
}}
>
<IconButton
variant='plain'
onClick={() => {
setIsDescriptionOpen(!isDescriptionOpen)
}}
size='sm'
sx={{
position: 'absolute',
bottom: 5,
right: 5,
}}
>
{isDescriptionOpen ? <CloseFullscreen /> : <OpenInFull />}
</IconButton>
<Box
sx={{
maxHeight: isDescriptionOpen ? 'none' : '100px',
overflowY: 'auto',
}}
>
<Typography level='body-md' sx={{ mb: 1 }}>
{chore.description || '--'}
</Typography>
</Box>
</Sheet>
</>
)}
{chore.notes && ( {chore.notes && (
<> <>
<Typography level='title-md' sx={{ mb: 1 }}> <Typography level='title-md' sx={{ mb: 1 }}>

View file

@ -300,6 +300,12 @@ const ChoreCard = ({
} }
} }
const getRecurrentChipText = chore => { const getRecurrentChipText = chore => {
// if chore.frequencyMetadata is type string then parse it otherwise assigned to the metadata:
const metadata =
typeof chore.frequencyMetadata === 'string'
? JSON.parse(chore.frequencyMetadata)
: chore.frequencyMetadata
const dayOfMonthSuffix = n => { const dayOfMonthSuffix = n => {
if (n >= 11 && n <= 13) { if (n >= 11 && n <= 13) {
return 'th' return 'th'
@ -330,7 +336,7 @@ const ChoreCard = ({
} else if (chore.frequencyType === 'yearly') { } else if (chore.frequencyType === 'yearly') {
return 'Yearly' return 'Yearly'
} else if (chore.frequencyType === 'days_of_the_week') { } else if (chore.frequencyType === 'days_of_the_week') {
let days = JSON.parse(chore.frequencyMetadata).days let days = metadata.days
if (days.length > 4) { if (days.length > 4) {
const allDays = [ const allDays = [
'Sunday', 'Sunday',
@ -354,7 +360,7 @@ const ChoreCard = ({
return days.join(', ') return days.join(', ')
} }
} else if (chore.frequencyType === 'day_of_the_month') { } else if (chore.frequencyType === 'day_of_the_month') {
let months = JSON.parse(chore.frequencyMetadata).months let months = metadata.months
if (months.length > 6) { if (months.length > 6) {
const allMonths = [ const allMonths = [
'January', 'January',
@ -385,16 +391,14 @@ const ChoreCard = ({
except ${notSelectedShortMonths.join(', ')}` except ${notSelectedShortMonths.join(', ')}`
return result return result
} else { } else {
let freqData = JSON.parse(chore.frequencyMetadata) let freqData = metadata
const months = freqData.months.map(m => moment().month(m).format('MMM')) const months = freqData.months.map(m => moment().month(m).format('MMM'))
return `${chore.frequency}${dayOfMonthSuffix( return `${chore.frequency}${dayOfMonthSuffix(
chore.frequency, chore.frequency,
)} of ${months.join(', ')}` )} of ${months.join(', ')}`
} }
} else if (chore.frequencyType === 'interval') { } else if (chore.frequencyType === 'interval') {
return `Every ${chore.frequency} ${ return `Every ${chore.frequency} ${metadata.unit}`
JSON.parse(chore.frequencyMetadata).unit
}`
} else { } else {
return chore.frequencyType return chore.frequencyType
} }

View file

@ -5,7 +5,7 @@ import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
const IconButtonWithMenu = ({ const IconButtonWithMenu = ({
label, label,
key, k,
icon, icon,
options, options,
onItemSelect, onItemSelect,
@ -72,14 +72,14 @@ const IconButtonWithMenu = ({
)} )}
<Menu <Menu
key={key} key={k}
ref={menuRef} ref={menuRef}
anchorEl={anchorEl} anchorEl={anchorEl}
open={Boolean(anchorEl)} open={Boolean(anchorEl)}
onClose={handleMenuClose} onClose={handleMenuClose}
> >
{title && ( {title && (
<MenuItem key={`${key}-title`} disabled> <MenuItem key={`${k}-title`} disabled>
<Typography level='body-sm' sx={{ fontWeight: 'bold' }}> <Typography level='body-sm' sx={{ fontWeight: 'bold' }}>
{title} {title}
</Typography> </Typography>
@ -87,7 +87,7 @@ const IconButtonWithMenu = ({
)} )}
{options?.map(item => ( {options?.map(item => (
<MenuItem <MenuItem
key={`${key}-${item?.id}`} key={`${k}-${item?.id}`}
onClick={() => { onClick={() => {
onItemSelect(item) onItemSelect(item)
setSelectedItem?.selectedItem(item.name) setSelectedItem?.selectedItem(item.name)

View file

@ -420,11 +420,7 @@ const MyChores = () => {
setActiveTextField('search') setActiveTextField('search')
setSearchInputFocus(searchInputFocus + 1) setSearchInputFocus(searchInputFocus + 1)
searchInputRef.current.focus() searchInputRef?.current?.focus()
searchInputRef.current.selectionStart =
searchInputRef.current.value?.length
searchInputRef.current.selectionEnd =
searchInputRef.current.value?.length
}} }}
> >
<Search /> <Search />
@ -434,6 +430,7 @@ const MyChores = () => {
<IconButtonWithMenu <IconButtonWithMenu
title='Group by' title='Group by'
k={'icon-menu-group-by'}
icon={<Sort />} icon={<Sort />}
options={[ options={[
{ name: 'Due Date', value: 'due_date' }, { name: 'Due Date', value: 'due_date' },
@ -456,7 +453,7 @@ const MyChores = () => {
<div className='grid flex-1 grid-cols-3 gap-4'> <div className='grid flex-1 grid-cols-3 gap-4'>
<IconButtonWithMenu <IconButtonWithMenu
label={' Priority'} label={' Priority'}
key={'icon-menu-labels-filter'} k={'icon-menu-priority-filter'}
icon={<PriorityHigh />} icon={<PriorityHigh />}
options={Priorities} options={Priorities}
selectedItem={selectedFilter} selectedItem={selectedFilter}
@ -468,7 +465,7 @@ const MyChores = () => {
/> />
<IconButtonWithMenu <IconButtonWithMenu
key={'icon-menu-labels-filter'} k={'icon-menu-labels-filter'}
label={' Labels'} label={' Labels'}
icon={<Style />} icon={<Style />}
options={userLabels} options={userLabels}

View file

@ -19,7 +19,9 @@ import { useContext, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { CSSTransition } from 'react-transition-group' import { CSSTransition } from 'react-transition-group'
import { UserContext } from '../../contexts/UserContext' import { UserContext } from '../../contexts/UserContext'
import useDebounce from '../../utils/Debounce'
import { CreateChore } from '../../utils/Fetcher' import { CreateChore } from '../../utils/Fetcher'
import { useLabels } from '../Labels/LabelQueries'
import LearnMoreButton from './LearnMore' import LearnMoreButton from './LearnMore'
const VALID_DAYS = { const VALID_DAYS = {
monday: 'Monday', monday: 'Monday',
@ -69,9 +71,11 @@ const ALL_MONTHS = Object.values(VALID_MONTHS).filter(
) )
const TaskInput = ({ autoFocus, onChoreUpdate }) => { const TaskInput = ({ autoFocus, onChoreUpdate }) => {
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
const { userProfile } = useContext(UserContext) const { userProfile } = useContext(UserContext)
const navigate = useNavigate() const navigate = useNavigate()
const [taskText, setTaskText] = useState('') const [taskText, setTaskText] = useState('')
const debounceParsing = useDebounce(taskText, 300)
const [taskTitle, setTaskTitle] = useState('') const [taskTitle, setTaskTitle] = useState('')
const [openModal, setOpenModal] = useState(false) const [openModal, setOpenModal] = useState(false)
const textareaRef = useRef(null) const textareaRef = useRef(null)
@ -98,6 +102,12 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
} }
}, [autoFocus]) }, [autoFocus])
useEffect(() => {
if (debounceParsing) {
processText(debounceParsing)
}
}, [debounceParsing])
const handleEnterPressed = e => { const handleEnterPressed = e => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
createChore() createChore()
@ -139,7 +149,28 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
} }
return { result: 0, cleanedSentence: inputSentence } return { result: 0, cleanedSentence: inputSentence }
} }
const parseLabels = inputSentence => {
let sentence = inputSentence.toLowerCase()
const currentLabels = []
// label will always be prefixed #:
for (const label of userLabels) {
if (sentence.includes(`#${label.name.toLowerCase()}`)) {
currentLabels.push(label)
sentence = sentence.replace(`#${label.name.toLowerCase()}`, '')
}
}
if (currentLabels.length > 0) {
return {
result: currentLabels,
cleanedSentence: sentence,
}
}
return { result: null, cleanedSentence: sentence }
}
const parseAssignee = inputSentence => {
let sentence = inputSentence.toLowerCase()
const assigneeMap = {}
}
const parseRepeatV2 = inputSentence => { const parseRepeatV2 = inputSentence => {
const sentence = inputSentence.toLowerCase() const sentence = inputSentence.toLowerCase()
const result = { const result = {
@ -164,6 +195,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
regex: /(every day|daily)$/i, regex: /(every day|daily)$/i,
name: 'Every day', name: 'Every day',
}, },
{
frequencyType: 'daily:time',
regex: /every (morning|noon|afternoon|evening|night)$/i,
name: 'Every {time} daily',
},
{ {
frequencyType: 'weekly', frequencyType: 'weekly',
regex: /(every week|weekly)$/i, regex: /(every week|weekly)$/i,
@ -284,8 +320,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
.replace('{months}', result.frequencyMetadata.months.join(', ')), .replace('{months}', result.frequencyMetadata.months.join(', ')),
cleanedSentence: inputSentence.replace(match[0], '').trim(), cleanedSentence: inputSentence.replace(match[0], '').trim(),
} }
case 'interval:every_other':
case 'interval:2week': case 'interval:2week':
result.frequency = 2 result.frequency = 2
result.frequencyMetadata.unit = 'weeks' result.frequencyMetadata.unit = 'weeks'
@ -295,6 +329,17 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
name: pattern.name, name: pattern.name,
cleanedSentence: inputSentence.replace(match[0], '').trim(), cleanedSentence: inputSentence.replace(match[0], '').trim(),
} }
case 'daily:time':
result.frequency = 1
result.frequencyMetadata.unit = 'days'
result.frequencyType = 'daily'
return {
result,
name: pattern.name.replace('{time}', match[1]),
// replace every x with ''
cleanedSentence: inputSentence.replace(match[0], '').trim(),
}
case 'day_of_the_month:every': case 'day_of_the_month:every':
result.frequency = parseInt(match[1], 10) result.frequency = parseInt(match[1], 10)
@ -307,6 +352,15 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
.replace('{months}', result.frequencyMetadata.months.join(', ')), .replace('{months}', result.frequencyMetadata.months.join(', ')),
cleanedSentence: inputSentence.replace(match[0], '').trim(), cleanedSentence: inputSentence.replace(match[0], '').trim(),
} }
case 'interval:every_other':
result.frequency = 2
result.frequencyMetadata.unit = match[1]
result.frequencyType = 'interval'
return {
result,
name: pattern.name.replace('{unit}', result.frequencyMetadata.unit),
cleanedSentence: inputSentence.replace(match[0], '').trim(),
}
} }
} }
return { result: null, name: null, cleanedSentence: inputSentence } return { result: null, name: null, cleanedSentence: inputSentence }
@ -321,8 +375,10 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
setPriority(0) setPriority(0)
return return
} }
setTaskText(e.target.value)
let cleanedSentence = e.target.value }
const processText = sentence => {
let cleanedSentence = sentence
const priority = parsePriority(cleanedSentence) const priority = parsePriority(cleanedSentence)
if (priority.result) setPriority(priority.result) if (priority.result) setPriority(priority.result)
cleanedSentence = priority.cleanedSentence cleanedSentence = priority.cleanedSentence
@ -344,11 +400,24 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
cleanedSentence = cleanedSentence.replace(parsedDueDate[0].text, '') cleanedSentence = cleanedSentence.replace(parsedDueDate[0].text, '')
} }
if (repeat.result) {
// if repeat has result the cleaned sentence will remove the date related info which mean
// we need to reparse the date again to get the correct due date:
const parsedDueDate = chrono.parse(sentence, new Date(), {
forwardDate: true,
})
if (parsedDueDate[0]?.index > -1) {
setDueDate(
moment(parsedDueDate[0].start.date()).format('YYYY-MM-DDTHH:mm:ss'),
)
}
}
if (priority.result || parsedDueDate[0]?.index > -1 || repeat.result) { if (priority.result || parsedDueDate[0]?.index > -1 || repeat.result) {
setOpenModal(true) setOpenModal(true)
} }
setTaskText(e.target.value) setTaskText(sentence)
setTaskTitle(cleanedSentence.trim()) setTaskTitle(cleanedSentence.trim())
} }
@ -504,6 +573,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
<FormControl> <FormControl>
<Typography level='body-sm'>Priority</Typography> <Typography level='body-sm'>Priority</Typography>
<Select <Select
defaultValue={0}
value={priority} value={priority}
onChange={(e, value) => setPriority(value)} onChange={(e, value) => setPriority(value)}
> >
@ -535,9 +605,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
> >
<FormControl> <FormControl>
<Typography level='body-sm'>Assignee</Typography> <Typography level='body-sm'>Assignee</Typography>
<Select value={'0'}> <Select value={'0'} disabled>
<Option value='0'>Me</Option> <Option value='0'>Me</Option>
<Option value='1'>Other</Option> {/* <Option value='1'>Other</Option> */}
</Select> </Select>
</FormControl> </FormControl>
<FormControl> <FormControl>

View file

@ -132,12 +132,7 @@ const CalendarView = ({ chores }) => {
mb: 0.4, mb: 0.4,
py: 1, py: 1,
px: 1, px: 1,
cursor: 'pointer',
// backgroundColor: getAssigneeColor(
// chore.assignedTo,
// userProfile,
// ),
// everything show in one row:
}} }}
> >
<CardContent> <CardContent>