import { Add } from '@mui/icons-material' import { Box, Button, Chip, Input, Modal, ModalDialog, ModalOverflow, Option, Select, Textarea, Typography, } from '@mui/joy' import { FormControl } from '@mui/material' import * as chrono from 'chrono-node' import moment from 'moment' import { useContext, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { UserContext } from '../../contexts/UserContext' import { useCreateChore } from '../../queries/ChoreQueries' import { useAllUsers } from '../../queries/UserQueries' import useDebounce from '../../utils/Debounce' import { isPlusAccount } from '../../utils/Helpers' import { useLabels } from '../Labels/LabelQueries' import SmartTaskTitleInput from '../TestView/SmartTaskTitleInput' import { parseLabels, parsePriority, parseRepeatV2 } from './CustomParsers' import LearnMoreButton from './LearnMore' import SubTasks from './SubTask' const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { const { data: userLabels, isLoading: userLabelsLoading } = useLabels() const { data: allUsers, isLoading: isAllUserLoading } = useAllUsers() const createChoreMutation = useCreateChore() const { userProfile } = useContext(UserContext) const navigate = useNavigate() const [taskText, setTaskText] = useState('') const debounceParsing = useDebounce(taskText, 30) const [taskTitle, setTaskTitle] = useState('') const [renderedParts, setRenderedParts] = useState([]) const textareaRef = useRef(null) const mainInputRef = useRef(null) const [priority, setPriority] = useState(0) const [dueDate, setDueDate] = useState(null) const [description, setDescription] = useState(null) const [assignedTo, setAssignedTo] = useState(userProfile?.id) const [assignees, setAssignees] = useState([]) const [labelsV2, setLabelsV2] = useState([]) const [frequency, setFrequency] = useState(null) const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null) const [subTasks, setSubTasks] = useState(null) const [hasDescription, setHasDescription] = useState(false) const [hasSubTasks, setHasSubTasks] = useState(false) useEffect(() => { if (isModalOpen && textareaRef.current) { textareaRef.current.focus() textareaRef.current.selectionStart = textareaRef.current.value?.length textareaRef.current.selectionEnd = textareaRef.current.value?.length } }, [isModalOpen]) useEffect(() => { if (autoFocus > 0 && mainInputRef.current) { mainInputRef.current.focus() mainInputRef.current.selectionStart = mainInputRef.current.value?.length mainInputRef.current.selectionEnd = mainInputRef.current.value?.length } }, [autoFocus]) useEffect(() => { if (!isModalOpen || userLabelsLoading || isAllUserLoading) { return } processText(taskText) }, [taskText, userLabelsLoading, isAllUserLoading]) const handleEnterPressed = () => { createChore() } const handleCloseModal = forceRefetch => { onClose(forceRefetch) setTaskText('') setTaskTitle('') setDueDate(null) setFrequency(null) setFrequencyHumanReadable(null) setPriority(0) setHasDescription(false) setDescription(null) setSubTasks(null) setHasSubTasks(false) setLabelsV2([]) } const handleSubmit = () => { createChore() handleCloseModal() setTaskText('') } const handleTextChange = e => { if (!e.target.value) { setTaskText('') setDueDate(null) setFrequency(null) setFrequencyHumanReadable(null) setPriority(0) return } setTaskText(e.target.value) } const processText = sentence => { let cleanedSentence = sentence const priority = parsePriority(sentence) if (priority.result) setPriority(priority.result) cleanedSentence = priority.cleanedSentence const labels = parseLabels(sentence, userLabels) if (labels.result) { cleanedSentence = labels.cleanedSentence setLabelsV2(labels.result) } const repeat = parseRepeatV2(sentence) if (repeat.result) { setFrequency(repeat.result) setFrequencyHumanReadable(repeat.name) cleanedSentence = repeat.cleanedSentence } // const assignees = parseAssignees(sentence) // if (assignees.result) { // cleanedSentence = assignees.cleanedSentence // set // } 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'), ) 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'), ) } } setTaskText(sentence) setTaskTitle(cleanedSentence.trim()) const rendered = renderText( sentence, repeat.highlight, priority.highlight, labels.highlight, parsedDueDate && parsedDueDate[0] ? { start: parsedDueDate[0].index, end: parsedDueDate[0].index + parsedDueDate[0].text.length, text: parsedDueDate[0].text, } : null, ) setRenderedParts(rendered) } const renderText = ( sentence, repeatHighlight, priorityHighlight, labelsHighlight, dueDateHighlight, ) => { const parts = [] let lastIndex = 0 // Combine all highlight ranges and sort them by their start index const allHighlights = [] if (repeatHighlight) { repeatHighlight.forEach(h => allHighlights.push({ ...h, type: 'repeat' })) } if (priorityHighlight) { priorityHighlight.forEach(h => allHighlights.push({ ...h, type: 'priority' }), ) } if (labelsHighlight) { labelsHighlight.forEach(h => allHighlights.push({ ...h, type: 'label' })) } if (dueDateHighlight) { allHighlights.push({ ...dueDateHighlight, type: 'dueDate' }) } allHighlights.sort((a, b) => a.start - b.start) for (const highlight of allHighlights) { // Add the text before the highlight if (highlight.start > lastIndex) { parts.push(sentence.substring(lastIndex, highlight.start)) } // Determine the class name based on the highlight type let className = '' switch (highlight.type) { case 'repeat': className = 'highlight-repeat' break case 'priority': className = 'highlight-priority' break case 'label': className = 'highlight-label' break case 'dueDate': className = 'highlight-date' break default: break } // Add the highlighted span parts.push( {sentence.substring(highlight.start, highlight.end)} , ) // Update the last index to the end of the current highlight lastIndex = highlight.end } // Add any remaining text after the last highlight if (lastIndex < sentence.length) { parts.push(sentence.substring(lastIndex)) } return parts } const createChore = () => { const chore = { name: taskTitle, assignees: [{ userId: userProfile.id }], dueDate: dueDate ? new Date(dueDate).toISOString() : null, assignedTo: userProfile.id, assignStrategy: 'random', isRolling: false, notification: false, description: description || null, labelsV2: labelsV2, priority: priority ? Number(priority) : 0, status: 0, frequencyType: 'once', frequencyMetadata: {}, notificationMetadata: {}, subTasks: subTasks?.length > 0 ? subTasks : null, } if (frequency) { chore.frequencyType = frequency.frequencyType chore.frequencyMetadata = frequency.frequencyMetadata chore.frequency = frequency.frequency if (isPlusAccount()) { chore.notification = true chore.notificationMetadata = { dueDate: true } } } if (!frequency && dueDate) { // use dueDate converted to UTC: chore.nextDueDate = new Date(dueDate).toUTCString() } createChoreMutation .mutateAsync(chore) .then(resp => { resp.json().then(data => { if (resp.status !== 200) { console.error('Error creating chore:', data) return } else { onChoreUpdate({ ...chore, id: data.res, nextDueDate: chore.dueDate, }) handleCloseModal(false) } }) }) .catch(error => { if (error?.queued) { handleCloseModal(true) } }) } if (userLabelsLoading || isAllUserLoading) { return <> } return ( Create new task Experimental Feature Task in a sentence: This feature lets you create a task simply by typing a sentence. It attempt parses the sentence to identify the task's due date, priority, and frequency. Examples:
  • Priority:For highest priority any of the following keyword P1, Urgent,{' '} Important, or ASAP. For lower priorities, use P2, P3, or P4 .
  • Due date: Specify dates with phrases like tomorrow, next week,{' '} Monday, or August 1st at 12pm.
  • Frequency: Set recurring tasks with terms like daily, weekly,{' '} monthly, yearly, or patterns such as{' '} every Tuesday and Thursday.
  • } />
    { setTaskText(text) }} customRenderer={renderedParts} onEnterPressed={handleEnterPressed} suggestions={{ '#': { value: 'id', display: 'name', options: userLabels ? userLabels : [], }, '!': ['P1', 'P2', 'P3', 'P4'], '@': { // value: 'userId', // display: 'displayName', // options: allUsers ? allUsers : [], value: 'id', display: 'name', options: [ { id: userProfile.id, name: userProfile.displayName }, ], }, }} />
    {/* Title: setTaskTitle(e.target.value)} sx={{ width: '100%', fontSize: '16px' }} /> */} {!hasDescription && ( )} {!hasSubTasks && ( )} {!dueDate && ( )} {hasDescription && ( Description: