enhance user input validation and parsing for usernames, points, and labels
This commit is contained in:
@@ -731,7 +731,7 @@ const NotificationTemplate = ({
|
||||
setShowSaveDefault(false)
|
||||
}}
|
||||
>
|
||||
Save Preference
|
||||
Remember for Future Tasks
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -259,6 +259,7 @@ export const useChoresHistory = (initialLimit, includeMembers) => {
|
||||
const resp = await GetChoresHistory(limit, includeMembers)
|
||||
return resp?.res || []
|
||||
},
|
||||
staleTime: 0,
|
||||
})
|
||||
|
||||
const handleLimitChange = newLimit => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// create boilerplate for ResetPasswordView:
|
||||
import Logo from '../../Logo'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Logo from '../../Logo'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { ResetPassword } from '../../utils/Fetcher'
|
||||
|
||||
@@ -77,10 +77,7 @@ const ForgotPasswordView = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
component='main'
|
||||
maxWidth='xs'
|
||||
>
|
||||
<Container component='main' maxWidth='xs'>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 4,
|
||||
@@ -110,17 +107,28 @@ const ForgotPasswordView = () => {
|
||||
</Typography>
|
||||
{resetStatusOk === null && (
|
||||
<>
|
||||
<Typography level='body2' sx={{ textAlign: 'center', mt: 2, mb: 3 }}>
|
||||
<Typography level='body2' sx={{ mb: 3 }}>
|
||||
Enter your email, and we'll send you a link to get into your
|
||||
account.
|
||||
</Typography>
|
||||
<FormControl error={emailError !== null} sx={{ width: '100%', mb: 2 }}>
|
||||
|
||||
<Typography level='body2' alignSelf={'start'} mb={1}>
|
||||
Email Address
|
||||
</Typography>
|
||||
<FormControl
|
||||
error={emailError !== null}
|
||||
sx={{ width: '100%', mb: 2 }}
|
||||
>
|
||||
<Input
|
||||
placeholder='Email'
|
||||
type='email'
|
||||
variant='soft'
|
||||
margin='normal'
|
||||
required
|
||||
fullWidth
|
||||
size='lg'
|
||||
id='email'
|
||||
placeholder='Enter your email address'
|
||||
type='email'
|
||||
name='email'
|
||||
autoComplete='email'
|
||||
autoFocus
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
error={emailError !== null}
|
||||
@@ -135,21 +143,30 @@ const ForgotPasswordView = () => {
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant='solid'
|
||||
size='lg'
|
||||
type='submit'
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
size='lg'
|
||||
variant='solid'
|
||||
sx={{
|
||||
width: '100%',
|
||||
mt: 3,
|
||||
mb: 2,
|
||||
border: 'moccasin',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
fullWidth
|
||||
size='lg'
|
||||
variant='plain'
|
||||
sx={{
|
||||
width: '100%',
|
||||
mb: 2,
|
||||
border: 'moccasin',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
@@ -164,10 +181,13 @@ const ForgotPasswordView = () => {
|
||||
)}
|
||||
{resetStatusOk != null && (
|
||||
<>
|
||||
<Typography level='body-md' sx={{ textAlign: 'center', mt: 2, mb: 3 }}>
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{ textAlign: 'center', mt: 2, mb: 3 }}
|
||||
>
|
||||
If there is an account associated with the email you entered,
|
||||
you will receive an email with instructions on how to reset
|
||||
your password.
|
||||
you will receive an email with instructions on how to reset your
|
||||
password.
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -85,10 +85,10 @@ const SignupView = () => {
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// username should only contain letters , numbers , dot and dash:
|
||||
if (!/^[a-zA-Z0-9.-]+$/.test(username)) {
|
||||
// username should only contain lowercase letters, dot and dash:
|
||||
if (!/^[a-z.-]+$/.test(username)) {
|
||||
setUsernameError(
|
||||
'Username can only contain letters, numbers, dot and dash',
|
||||
'Username can only contain lowercase letters, dot and dash',
|
||||
)
|
||||
isValid = false
|
||||
}
|
||||
|
||||
@@ -29,17 +29,14 @@ import {
|
||||
useArchiveChore,
|
||||
useChore,
|
||||
useCreateChore,
|
||||
useDeleteChores,
|
||||
useUnArchiveChore,
|
||||
useUpdateChore,
|
||||
} from '../../queries/ChoreQueries.jsx'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import {
|
||||
DeleteChore,
|
||||
GetAllCircleMembers,
|
||||
GetThings,
|
||||
} from '../../utils/Fetcher'
|
||||
import { GetAllCircleMembers, GetThings } from '../../utils/Fetcher'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import Priorities from '../../utils/Priorities.jsx'
|
||||
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
|
||||
@@ -117,6 +114,7 @@ const ChoreEdit = () => {
|
||||
const createChoreMutation = useCreateChore()
|
||||
const archiveChore = useArchiveChore()
|
||||
const unarchiveChore = useUnArchiveChore()
|
||||
const deleteChores = useDeleteChores()
|
||||
const {
|
||||
data: choreData,
|
||||
isLoading: isChoreLoading,
|
||||
@@ -432,12 +430,16 @@ const ChoreEdit = () => {
|
||||
message: 'Are you sure you want to delete this chore?',
|
||||
onClose: isConfirmed => {
|
||||
if (isConfirmed === true) {
|
||||
DeleteChore(choreId).then(response => {
|
||||
if (response.status === 200) {
|
||||
deleteChores.mutate([choreId], {
|
||||
onSuccess: () => {
|
||||
Navigate('/chores')
|
||||
} else {
|
||||
alert('Failed to delete chore')
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
showError({
|
||||
title: 'Delete Failed',
|
||||
message: `Failed to delete chore: ${error.message}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
setConfirmModelConfig({})
|
||||
@@ -727,7 +729,7 @@ const ChoreEdit = () => {
|
||||
setShowSaveAssigneeDefault(false)
|
||||
}}
|
||||
>
|
||||
Save Assignee Preference
|
||||
Remember for Future Tasks
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
@@ -1229,7 +1231,7 @@ const ChoreEdit = () => {
|
||||
setShowSavePrivacyDefault(false)
|
||||
}}
|
||||
>
|
||||
Save Preference
|
||||
Remember for Future Tasks
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Input,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
@@ -30,9 +30,9 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
newErrors.childName = 'Sub account name must be at least 2 characters'
|
||||
} else if (childName.length > 20) {
|
||||
newErrors.childName = 'Sub account name must be less than 20 characters'
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(childName)) {
|
||||
} else if (!/^[a-z.-]+$/.test(childName)) {
|
||||
newErrors.childName =
|
||||
'Sub account name can only contain letters, numbers, and underscores'
|
||||
'Sub account name can only contain lowercase letters, dot and dash'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +133,6 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
|
||||
{errors.childName && (
|
||||
<FormHelperText>{errors.childName}</FormHelperText>
|
||||
)}
|
||||
<FormHelperText>
|
||||
This will create a username like: primaryname_subaccountname
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl error={!!errors.displayName} sx={{ mb: 2 }}>
|
||||
|
||||
@@ -882,10 +882,7 @@ const UserActivites = () => {
|
||||
)
|
||||
}}
|
||||
renderValue={() => {
|
||||
if (
|
||||
selectedUser === undefined ||
|
||||
selectedUser === 'all'
|
||||
) {
|
||||
if (selectedUser === undefined || selectedUser === 'all') {
|
||||
return (
|
||||
<Typography
|
||||
startDecorator={
|
||||
@@ -917,9 +914,8 @@ const UserActivites = () => {
|
||||
}
|
||||
>
|
||||
{
|
||||
circleUsers.find(
|
||||
user => user.userId === selectedUser,
|
||||
)?.displayName
|
||||
circleUsers.find(user => user.userId === selectedUser)
|
||||
?.displayName
|
||||
}
|
||||
</Typography>
|
||||
)
|
||||
@@ -1048,7 +1044,7 @@ const UserActivites = () => {
|
||||
</Box>
|
||||
|
||||
{/* Conditional Content Based on Data Availability */}
|
||||
{(!choresData.res?.length > 0 || !choresHistory?.length > 0) ? (
|
||||
{!choresData.res?.length > 0 || !choresHistory?.length > 0 ? (
|
||||
<Container
|
||||
maxWidth='md'
|
||||
sx={{
|
||||
@@ -1167,7 +1163,11 @@ const UserActivites = () => {
|
||||
<Typography level='h4' textAlign='center' sx={{ mb: 1 }}>
|
||||
{chartData[selectedChart].title}
|
||||
</Typography>
|
||||
<Typography level='body-xs' textAlign='center' sx={{ mb: 2 }}>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
textAlign='center'
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{chartData[selectedChart].description}
|
||||
</Typography>
|
||||
<Box
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
parseAssignees,
|
||||
parseDueDate,
|
||||
parseLabels,
|
||||
parsePoints,
|
||||
parsePriority,
|
||||
parseRepeatV2,
|
||||
} from './CustomParsers'
|
||||
@@ -68,6 +69,8 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
})
|
||||
const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null)
|
||||
const [subTasks, setSubTasks] = useState(null)
|
||||
const [points, setPoints] = useState(-1)
|
||||
const [isAnyoneTask, setIsAnyoneTask] = useState(false)
|
||||
const [hasDescription, setHasDescription] = useState(false)
|
||||
const [hasSubTasks, setHasSubTasks] = useState(false)
|
||||
const [hasNotifications, setHasNotifications] = useState(false)
|
||||
@@ -170,6 +173,8 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
priorityHighlight,
|
||||
labelsHighlight,
|
||||
dueDateHighlight,
|
||||
pointsHighlight,
|
||||
assigneesHighlight,
|
||||
) => {
|
||||
const parts = []
|
||||
let lastIndex = 0
|
||||
@@ -179,24 +184,34 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
const allHighlights = []
|
||||
if (repeatHighlight) {
|
||||
repeatHighlight.forEach(h =>
|
||||
allHighlights.push({ ...h, type: 'repeat', priority: 40 }),
|
||||
allHighlights.push({ ...h, type: 'repeat', priority: 60 }),
|
||||
)
|
||||
}
|
||||
if (priorityHighlight) {
|
||||
priorityHighlight.forEach(h =>
|
||||
allHighlights.push({ ...h, type: 'priority', priority: 30 }),
|
||||
allHighlights.push({ ...h, type: 'priority', priority: 50 }),
|
||||
)
|
||||
}
|
||||
if (pointsHighlight) {
|
||||
pointsHighlight.forEach(h =>
|
||||
allHighlights.push({ ...h, type: 'points', priority: 45 }),
|
||||
)
|
||||
}
|
||||
if (assigneesHighlight) {
|
||||
assigneesHighlight.forEach(h =>
|
||||
allHighlights.push({ ...h, type: 'assignee', priority: 40 }),
|
||||
)
|
||||
}
|
||||
if (labelsHighlight) {
|
||||
labelsHighlight.forEach(h =>
|
||||
allHighlights.push({ ...h, type: 'label', priority: 20 }),
|
||||
allHighlights.push({ ...h, type: 'label', priority: 30 }),
|
||||
)
|
||||
}
|
||||
if (dueDateHighlight) {
|
||||
allHighlights.push({
|
||||
...dueDateHighlight,
|
||||
type: 'dueDate',
|
||||
priority: 10,
|
||||
priority: 20,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -234,6 +249,12 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
case 'priority':
|
||||
className = 'highlight-priority'
|
||||
break
|
||||
case 'points':
|
||||
className = 'highlight-points'
|
||||
break
|
||||
case 'assignee':
|
||||
className = 'highlight-assignee'
|
||||
break
|
||||
case 'label':
|
||||
className = 'highlight-label'
|
||||
break
|
||||
@@ -286,22 +307,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
|
||||
const processText = useCallback(
|
||||
sentence => {
|
||||
let cleanedSentence = sentence
|
||||
// Parse everything from the original sentence to get correct highlight positions
|
||||
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 pointsParsed = parsePoints(sentence)
|
||||
const labels = parseLabels(sentence, userLabels || [])
|
||||
|
||||
const repeat = parseRepeatV2(sentence)
|
||||
if (repeat.result) {
|
||||
setFrequency(repeat.result)
|
||||
setFrequencyHumanReadable(repeat.name)
|
||||
cleanedSentence = repeat.cleanedSentence
|
||||
}
|
||||
// Parse assignees using circle members
|
||||
const circleMembersList = circleMembers?.res || []
|
||||
const assigneesForParsing = circleMembersList.map(member => ({
|
||||
@@ -315,28 +325,44 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
}))
|
||||
|
||||
const assigneesResult = parseAssignees(sentence, assigneesForParsing)
|
||||
if (assigneesResult.result) {
|
||||
cleanedSentence = assigneesResult.cleanedSentence
|
||||
console.log('CLEANED', cleanedSentence)
|
||||
const repeat = parseRepeatV2(sentence)
|
||||
const dueDateParsed = parseDueDate(sentence, chrono)
|
||||
|
||||
setAssignees(
|
||||
assigneesResult.result.map(assignee => ({
|
||||
// Set all the parsed values
|
||||
if (priority.result) setPriority(priority.result)
|
||||
if (pointsParsed.result) setPoints(pointsParsed.result)
|
||||
if (labels.result) setLabelsV2(labels.result)
|
||||
|
||||
if (assigneesResult.isAnyone) {
|
||||
// @Anyone was used - set empty assignees (anyone can do the task)
|
||||
setIsAnyoneTask(true)
|
||||
setAssignees([])
|
||||
} else if (assigneesResult.result && assigneesResult.result.length > 0) {
|
||||
setIsAnyoneTask(false)
|
||||
const parsedAssignees = assigneesResult.result.map(assignee => ({
|
||||
userId: assignee.userId,
|
||||
})),
|
||||
)
|
||||
}))
|
||||
setAssignees(parsedAssignees)
|
||||
} else {
|
||||
// Only assign to current user if no @ mentions found and userProfile exists
|
||||
setIsAnyoneTask(false)
|
||||
if (userProfile?.id) {
|
||||
setAssignees([
|
||||
{
|
||||
userId: userProfile.id,
|
||||
},
|
||||
])
|
||||
}
|
||||
// Parse due date
|
||||
const dueDateParsed = parseDueDate(sentence, chrono)
|
||||
}
|
||||
|
||||
if (repeat.result) {
|
||||
setFrequency(repeat.result)
|
||||
setFrequencyHumanReadable(repeat.name)
|
||||
}
|
||||
|
||||
let dueDateHighlight = null
|
||||
if (dueDateParsed.result) {
|
||||
setDueDate(moment(dueDateParsed.result).format('YYYY-MM-DDTHH:mm:ss'))
|
||||
cleanedSentence = dueDateParsed.cleanedSentence
|
||||
dueDateHighlight = dueDateParsed.highlight[0]
|
||||
}
|
||||
|
||||
@@ -351,24 +377,69 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Create the cleaned sentence by sequentially applying all cleanups
|
||||
let cleanedSentence = sentence
|
||||
if (priority.result) cleanedSentence = priority.cleanedSentence
|
||||
if (pointsParsed.result) {
|
||||
// Apply points cleaning to the current cleaned sentence
|
||||
const pointsReparse = parsePoints(cleanedSentence)
|
||||
if (pointsReparse.result)
|
||||
cleanedSentence = pointsReparse.cleanedSentence
|
||||
}
|
||||
if (labels.result) {
|
||||
// Apply labels cleaning to the current cleaned sentence
|
||||
const labelsReparse = parseLabels(cleanedSentence, userLabels || [])
|
||||
if (labelsReparse.result)
|
||||
cleanedSentence = labelsReparse.cleanedSentence
|
||||
}
|
||||
if (assigneesResult.result) {
|
||||
// Apply assignees cleaning to the current cleaned sentence
|
||||
const assigneesReparse = parseAssignees(
|
||||
cleanedSentence,
|
||||
assigneesForParsing,
|
||||
)
|
||||
if (assigneesReparse.result)
|
||||
cleanedSentence = assigneesReparse.cleanedSentence
|
||||
}
|
||||
if (repeat.result) {
|
||||
// Apply repeat cleaning to the current cleaned sentence
|
||||
const repeatReparse = parseRepeatV2(cleanedSentence)
|
||||
if (repeatReparse.result)
|
||||
cleanedSentence = repeatReparse.cleanedSentence
|
||||
}
|
||||
if (dueDateParsed.result) {
|
||||
// Apply date cleaning to the current cleaned sentence
|
||||
const dueDateReparse = parseDueDate(cleanedSentence, chrono)
|
||||
if (dueDateReparse.result)
|
||||
cleanedSentence = dueDateReparse.cleanedSentence
|
||||
}
|
||||
|
||||
setTaskText(sentence)
|
||||
setTaskTitle(cleanedSentence.trim())
|
||||
const { parts, plainText } = renderHighlightedSentence(
|
||||
|
||||
// Generate highlights for rendering using original sentence positions
|
||||
const { parts } = renderHighlightedSentence(
|
||||
sentence,
|
||||
repeat.highlight,
|
||||
priority.highlight,
|
||||
labels.highlight,
|
||||
dueDateHighlight,
|
||||
pointsParsed.highlight,
|
||||
assigneesResult.highlight,
|
||||
)
|
||||
|
||||
setRenderedParts(parts)
|
||||
setTaskTitle(plainText)
|
||||
},
|
||||
[userLabels, renderHighlightedSentence],
|
||||
[userLabels, renderHighlightedSentence, circleMembers, userProfile],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen || userLabelsLoading || isCircleMembersLoading) {
|
||||
if (
|
||||
!isModalOpen ||
|
||||
userLabelsLoading ||
|
||||
isCircleMembersLoading ||
|
||||
!userProfile
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -378,6 +449,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
userLabelsLoading,
|
||||
isCircleMembersLoading,
|
||||
isModalOpen,
|
||||
userProfile,
|
||||
processText,
|
||||
])
|
||||
|
||||
@@ -393,6 +465,8 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
setFrequency(null)
|
||||
setFrequencyHumanReadable(null)
|
||||
setPriority(0)
|
||||
setPoints(-1)
|
||||
setIsAnyoneTask(false)
|
||||
setHasDescription(false)
|
||||
setDescription(null)
|
||||
setSubTasks(null)
|
||||
@@ -402,17 +476,41 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
}
|
||||
|
||||
const createChore = () => {
|
||||
// Handle different assignee scenarios
|
||||
let finalAssignees = assignees
|
||||
let finalAssignedTo = null
|
||||
let finalAssignStrategy = 'random'
|
||||
|
||||
if (isAnyoneTask) {
|
||||
// @Anyone was explicitly used - anyone can do the task
|
||||
finalAssignees = []
|
||||
finalAssignedTo = null
|
||||
finalAssignStrategy = 'no_assignee'
|
||||
} else if (assignees.length === 0) {
|
||||
// No assignees and no @Anyone - fallback to current user
|
||||
finalAssignees = [{ userId: userProfile?.id }]
|
||||
finalAssignedTo = userProfile?.id
|
||||
finalAssignStrategy = 'keep_last_assigned'
|
||||
} else if (assignees.length === 1) {
|
||||
// Single assignee
|
||||
finalAssignedTo = assignees[0].userId
|
||||
finalAssignStrategy = 'keep_last_assigned'
|
||||
} else {
|
||||
// Multiple assignees
|
||||
finalAssignedTo = null
|
||||
finalAssignStrategy = 'random'
|
||||
}
|
||||
|
||||
const chore = {
|
||||
name: taskTitle,
|
||||
assignees:
|
||||
assignees.length > 0 ? assignees : [{ userId: userProfile.id }],
|
||||
assignees: finalAssignees,
|
||||
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
|
||||
assignedTo: assignees.length > 0 ? assignees[0].userId : userProfile.id,
|
||||
assignStrategy: 'random',
|
||||
assignedTo: finalAssignedTo,
|
||||
assignStrategy: finalAssignStrategy,
|
||||
isRolling: false,
|
||||
|
||||
labelsV2: labelsV2,
|
||||
priority: priority ? Number(priority) : 0,
|
||||
points: points > -1 ? points : null,
|
||||
status: 0,
|
||||
frequencyType: 'once',
|
||||
frequencyMetadata: {},
|
||||
@@ -552,7 +650,22 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
'@': {
|
||||
value: 'userId',
|
||||
display: 'displayName',
|
||||
options: circleMembers?.res || [],
|
||||
options: [
|
||||
{ userId: 'anyone', displayName: 'Anyone' },
|
||||
...(circleMembers?.res || []),
|
||||
],
|
||||
},
|
||||
'*': {
|
||||
value: 'id',
|
||||
display: 'name',
|
||||
options: [
|
||||
{ id: '1', name: '1 point' },
|
||||
{ id: '5', name: '5 points' },
|
||||
{ id: '10', name: '10 points' },
|
||||
{ id: '25', name: '25 points' },
|
||||
{ id: '50', name: '50 points' },
|
||||
{ id: '100', name: '100 points' },
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -85,35 +85,63 @@ export const parsePriority = inputSentence => {
|
||||
export const parseLabels = (inputSentence, userLabels) => {
|
||||
let sentence = inputSentence.toLowerCase()
|
||||
const currentLabels = []
|
||||
// label will always be prefixed #:
|
||||
const newLabels = []
|
||||
const allHighlights = []
|
||||
|
||||
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,
|
||||
highlight: currentLabels.map(label => {
|
||||
const index = inputSentence
|
||||
.toLowerCase()
|
||||
.indexOf(`#${label.name.toLowerCase()}`)
|
||||
return {
|
||||
text: `#${label.name}`,
|
||||
start: index,
|
||||
end: index + label.name.length + 1,
|
||||
}
|
||||
}),
|
||||
// Find all #label patterns in the sentence
|
||||
const labelPattern = /#(\w+)/gi
|
||||
const matches = [...inputSentence.matchAll(labelPattern)]
|
||||
|
||||
cleanedSentence: sentence.replace(
|
||||
new RegExp(`#(${userLabels.map(l => l.name).join('|')})`, 'g'),
|
||||
'',
|
||||
),
|
||||
for (const match of matches) {
|
||||
const labelName = match[1]
|
||||
const fullMatch = match[0]
|
||||
const startIndex = match.index
|
||||
|
||||
// Check if this label already exists
|
||||
const existingLabel = userLabels.find(
|
||||
label => label.name.toLowerCase() === labelName.toLowerCase(),
|
||||
)
|
||||
|
||||
if (existingLabel) {
|
||||
currentLabels.push(existingLabel)
|
||||
} else {
|
||||
// Create a new label object for new labels
|
||||
newLabels.push({
|
||||
name: labelName,
|
||||
color: '#3b82f6', // Default blue color
|
||||
isNew: true,
|
||||
})
|
||||
}
|
||||
|
||||
allHighlights.push({
|
||||
text: fullMatch,
|
||||
start: startIndex,
|
||||
end: startIndex + fullMatch.length,
|
||||
})
|
||||
|
||||
// Remove the label from the sentence
|
||||
sentence = sentence.replace(fullMatch.toLowerCase(), '')
|
||||
}
|
||||
|
||||
const allLabels = [...currentLabels, ...newLabels]
|
||||
|
||||
if (allLabels.length > 0) {
|
||||
return {
|
||||
result: allLabels,
|
||||
newLabels: newLabels,
|
||||
highlight: allHighlights,
|
||||
cleanedSentence: inputSentence
|
||||
.replace(labelPattern, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim(),
|
||||
}
|
||||
}
|
||||
return { result: null, cleanedSentence: sentence }
|
||||
|
||||
return {
|
||||
result: null,
|
||||
newLabels: [],
|
||||
cleanedSentence: inputSentence,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseRepeatV2 = inputSentence => {
|
||||
@@ -498,37 +526,135 @@ export const parseAssignees = (inputSentence, users) => {
|
||||
const sentence = inputSentence.toLowerCase()
|
||||
const result = []
|
||||
const highlight = []
|
||||
// sort users by the longest so we remove first the full match:
|
||||
for (const user of users.sort(
|
||||
(a, b) => b.displayName.length - a.displayName.length,
|
||||
)) {
|
||||
if (sentence.includes(`@${user.displayName.toLowerCase()}`)) {
|
||||
result.push(user)
|
||||
const index = inputSentence
|
||||
.toLowerCase()
|
||||
.indexOf(`@${user.displayName.toLowerCase()}`)
|
||||
const matchedTexts = []
|
||||
|
||||
// Check for @Anyone first (special case)
|
||||
const anyoneRegex = /@anyone(?=\s|$)/i
|
||||
const anyoneMatch = inputSentence.match(anyoneRegex)
|
||||
|
||||
if (anyoneMatch) {
|
||||
const index = inputSentence.search(anyoneRegex)
|
||||
highlight.push({
|
||||
text: `@${user.displayName}`,
|
||||
text: anyoneMatch[0],
|
||||
start: index,
|
||||
end: index + user.displayName.length + 1,
|
||||
end: index + anyoneMatch[0].length,
|
||||
})
|
||||
matchedTexts.push({ pattern: '@anyone', original: anyoneMatch[0] })
|
||||
|
||||
// For @Anyone, return empty result (no specific assignees)
|
||||
let cleanedSentence = inputSentence
|
||||
for (const matchedText of matchedTexts) {
|
||||
cleanedSentence = cleanedSentence.replace(
|
||||
new RegExp(
|
||||
matchedText.original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
||||
'gi',
|
||||
),
|
||||
'',
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
result: [], // Empty assignees for @Anyone
|
||||
isAnyone: true, // Flag to indicate @Anyone was used
|
||||
highlight,
|
||||
cleanedSentence: cleanedSentence.replace(/\s+/g, ' ').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
// Sort users by longest displayName first to avoid partial matches
|
||||
const sortedUsers = users.sort(
|
||||
(a, b) => (b.displayName?.length || 0) - (a.displayName?.length || 0),
|
||||
)
|
||||
|
||||
for (const user of sortedUsers) {
|
||||
if (!user.displayName) continue
|
||||
|
||||
// Only match on display name - use word boundaries for exact matching
|
||||
const displayNamePattern = `@${user.displayName.toLowerCase()}`
|
||||
// Use word boundary or space/end to ensure exact match, not partial
|
||||
const exactMatchRegex = new RegExp(
|
||||
`@${user.displayName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=\\s|$)`,
|
||||
'i',
|
||||
)
|
||||
const exactMatch = inputSentence.match(exactMatchRegex)
|
||||
|
||||
if (exactMatch && !result.some(r => r.userId === user.userId)) {
|
||||
result.push(user)
|
||||
const index = inputSentence.search(exactMatchRegex)
|
||||
|
||||
highlight.push({
|
||||
text: exactMatch[0],
|
||||
start: index,
|
||||
end: index + exactMatch[0].length,
|
||||
})
|
||||
matchedTexts.push({
|
||||
pattern: displayNamePattern,
|
||||
original: exactMatch[0],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length > 0) {
|
||||
return {
|
||||
result,
|
||||
highlight,
|
||||
cleanedSentence: sentence.replace(
|
||||
let cleanedSentence = inputSentence
|
||||
// Remove all matched assignee patterns using the original matched text
|
||||
for (const matchedText of matchedTexts) {
|
||||
cleanedSentence = cleanedSentence.replace(
|
||||
new RegExp(
|
||||
`@(${result.map(u => u.displayName.toLowerCase()).join('|')})`,
|
||||
'g',
|
||||
matchedText.original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
||||
'gi',
|
||||
),
|
||||
'',
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
result,
|
||||
isAnyone: false,
|
||||
highlight,
|
||||
cleanedSentence: cleanedSentence.replace(/\s+/g, ' ').trim(),
|
||||
}
|
||||
}
|
||||
return { result: null, cleanedSentence: sentence }
|
||||
|
||||
return { result: null, isAnyone: false, cleanedSentence: inputSentence }
|
||||
}
|
||||
|
||||
export const parsePoints = inputSentence => {
|
||||
let sentence = inputSentence.toLowerCase()
|
||||
const pointsPattern = /\*(\d+)\s*(?:points?)?/gi
|
||||
const match = sentence.match(pointsPattern)
|
||||
|
||||
if (!match) {
|
||||
return {
|
||||
result: null,
|
||||
highlight: [],
|
||||
cleanedSentence: inputSentence,
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the first points match
|
||||
const pointsMatch = match[0]
|
||||
const pointsValue = parseInt(pointsMatch.replace(/\D/g, ''), 10)
|
||||
const startIndex = inputSentence
|
||||
.toLowerCase()
|
||||
.indexOf(pointsMatch.toLowerCase())
|
||||
|
||||
return {
|
||||
result: pointsValue,
|
||||
highlight: [
|
||||
{
|
||||
text: pointsMatch,
|
||||
start: startIndex,
|
||||
end: startIndex + pointsMatch.length,
|
||||
},
|
||||
],
|
||||
cleanedSentence: inputSentence
|
||||
.replace(
|
||||
new RegExp(pointsMatch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'),
|
||||
'',
|
||||
)
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
export const parseDueDate = (inputSentence, chrono) => {
|
||||
|
||||
Reference in New Issue
Block a user